|
| 1 | +//go:build evm |
| 2 | + |
| 3 | +package e2e |
| 4 | + |
| 5 | +import ( |
| 6 | + "bytes" |
| 7 | + "context" |
| 8 | + "encoding/json" |
| 9 | + "fmt" |
| 10 | + "net/http" |
| 11 | + "os" |
| 12 | + "path/filepath" |
| 13 | + "testing" |
| 14 | + "time" |
| 15 | + |
| 16 | + "github.com/ethereum/go-ethereum/common" |
| 17 | + "github.com/ethereum/go-ethereum/ethclient" |
| 18 | + "github.com/stretchr/testify/require" |
| 19 | + |
| 20 | + "github.com/evstack/ev-node/execution/evm" |
| 21 | +) |
| 22 | + |
| 23 | +// enableForceInclusionInGenesis modifies the genesis file to set the force inclusion epoch |
| 24 | +// to a small value suitable for testing. |
| 25 | +func enableForceInclusionInGenesis(t *testing.T, homeDir string, epoch uint64) { |
| 26 | + t.Helper() |
| 27 | + genesisPath := filepath.Join(homeDir, "config", "genesis.json") |
| 28 | + data, err := os.ReadFile(genesisPath) |
| 29 | + require.NoError(t, err) |
| 30 | + |
| 31 | + var genesis map[string]interface{} |
| 32 | + err = json.Unmarshal(data, &genesis) |
| 33 | + require.NoError(t, err) |
| 34 | + |
| 35 | + genesis["da_epoch_forced_inclusion"] = epoch |
| 36 | + |
| 37 | + newData, err := json.MarshalIndent(genesis, "", " ") |
| 38 | + require.NoError(t, err) |
| 39 | + |
| 40 | + err = os.WriteFile(genesisPath, newData, 0644) |
| 41 | + require.NoError(t, err) |
| 42 | +} |
| 43 | + |
| 44 | +// submitForceInclusionTx sends a raw transaction to the force inclusion server |
| 45 | +func submitForceInclusionTx(t *testing.T, fiUrl string, txBytes []byte) { |
| 46 | + t.Helper() |
| 47 | + reqBody := map[string]interface{}{ |
| 48 | + "jsonrpc": "2.0", |
| 49 | + "id": 1, |
| 50 | + "method": "eth_sendRawTransaction", |
| 51 | + "params": []string{"0x" + common.Bytes2Hex(txBytes)}, |
| 52 | + } |
| 53 | + |
| 54 | + jsonData, err := json.Marshal(reqBody) |
| 55 | + require.NoError(t, err) |
| 56 | + |
| 57 | + resp, err := http.Post(fiUrl, "application/json", bytes.NewBuffer(jsonData)) |
| 58 | + require.NoError(t, err) |
| 59 | + defer resp.Body.Close() |
| 60 | + |
| 61 | + require.Equal(t, http.StatusOK, resp.StatusCode) |
| 62 | + |
| 63 | + var res map[string]interface{} |
| 64 | + err = json.NewDecoder(resp.Body).Decode(&res) |
| 65 | + require.NoError(t, err) |
| 66 | + require.Nil(t, res["error"], "RPC returned error: %v", res["error"]) |
| 67 | + require.NotNil(t, res["result"], "RPC result is nil") |
| 68 | +} |
| 69 | + |
| 70 | +// setupSequencerWithForceInclusion sets up a sequencer node with force inclusion enabled |
| 71 | +func setupSequencerWithForceInclusion(t *testing.T, sut *SystemUnderTest, nodeHome string, fiPort int) (string, string) { |
| 72 | + t.Helper() |
| 73 | + |
| 74 | + // Use common setup (no full node needed initially) |
| 75 | + jwtSecret, _, genesisHash, endpoints := setupCommonEVMTest(t, sut, false) |
| 76 | + |
| 77 | + // Create passphrase file |
| 78 | + passphraseFile := createPassphraseFile(t, nodeHome) |
| 79 | + |
| 80 | + // Create JWT secret file |
| 81 | + jwtSecretFile := createJWTSecretFile(t, nodeHome, jwtSecret) |
| 82 | + |
| 83 | + // Initialize sequencer node |
| 84 | + output, err := sut.RunCmd(evmSingleBinaryPath, |
| 85 | + "init", |
| 86 | + "--evnode.node.aggregator=true", |
| 87 | + "--evnode.signer.passphrase_file", passphraseFile, |
| 88 | + "--home", nodeHome, |
| 89 | + ) |
| 90 | + require.NoError(t, err, "failed to init sequencer", output) |
| 91 | + |
| 92 | + // Modify genesis to lower the epoch for faster testing (2 DA blocks) |
| 93 | + enableForceInclusionInGenesis(t, nodeHome, 2) |
| 94 | + |
| 95 | + // Start sequencer with force inclusion server enabled |
| 96 | + fiAddr := fmt.Sprintf("127.0.0.1:%d", fiPort) |
| 97 | + args := []string{ |
| 98 | + "start", |
| 99 | + "--evm.jwt-secret-file", jwtSecretFile, |
| 100 | + "--evm.genesis-hash", genesisHash, |
| 101 | + "--evnode.node.block_time", DefaultBlockTime, |
| 102 | + "--evnode.node.aggregator=true", |
| 103 | + "--evnode.signer.passphrase_file", passphraseFile, |
| 104 | + "--home", nodeHome, |
| 105 | + "--evnode.da.block_time", DefaultDABlockTime, |
| 106 | + "--evnode.da.address", endpoints.GetDAAddress(), |
| 107 | + "--evnode.da.namespace", DefaultDANamespace, |
| 108 | + "--evnode.da.forced_inclusion_namespace", "forced-inc", |
| 109 | + "--evnode.rpc.address", endpoints.GetRollkitRPCListen(), |
| 110 | + "--evnode.p2p.listen_address", endpoints.GetRollkitP2PAddress(), |
| 111 | + "--evm.engine-url", endpoints.GetSequencerEngineURL(), |
| 112 | + "--evm.eth-url", endpoints.GetSequencerEthURL(), |
| 113 | + "--force-inclusion-server", fiAddr, |
| 114 | + } |
| 115 | + sut.ExecCmd(evmSingleBinaryPath, args...) |
| 116 | + sut.AwaitNodeUp(t, endpoints.GetRollkitRPCAddress(), NodeStartupTimeout) |
| 117 | + |
| 118 | + return genesisHash, endpoints.GetSequencerEthURL() |
| 119 | +} |
| 120 | + |
| 121 | +func TestEvmSequencerForceInclusionE2E(t *testing.T) { |
| 122 | + sut := NewSystemUnderTest(t) |
| 123 | + workDir := t.TempDir() |
| 124 | + sequencerHome := filepath.Join(workDir, "sequencer") |
| 125 | + |
| 126 | + // Get a port for force inclusion server |
| 127 | + fiPort, err := getAvailablePort() |
| 128 | + require.NoError(t, err) |
| 129 | + fiUrl := fmt.Sprintf("http://127.0.0.1:%d", fiPort) |
| 130 | + |
| 131 | + // Setup sequencer with force inclusion enabled |
| 132 | + genesisHash, seqEthURL := setupSequencerWithForceInclusion(t, sut, sequencerHome, fiPort) |
| 133 | + t.Logf("Sequencer started with force inclusion server at %s", fiUrl) |
| 134 | + t.Logf("Genesis hash: %s", genesisHash) |
| 135 | + |
| 136 | + // Connect to sequencer EVM |
| 137 | + client, err := ethclient.Dial(seqEthURL) |
| 138 | + require.NoError(t, err) |
| 139 | + defer client.Close() |
| 140 | + |
| 141 | + // 1. Send a normal transaction first to ensure chain is moving |
| 142 | + t.Log("Sending normal transaction...") |
| 143 | + var nonce uint64 = 0 |
| 144 | + txNormal := evm.GetRandomTransaction(t, TestPrivateKey, TestToAddress, DefaultChainID, DefaultGasLimit, &nonce) |
| 145 | + err = client.SendTransaction(context.Background(), txNormal) |
| 146 | + require.NoError(t, err) |
| 147 | + |
| 148 | + require.Eventually(t, func() bool { |
| 149 | + return evm.CheckTxIncluded(client, txNormal.Hash()) |
| 150 | + }, 15*time.Second, 500*time.Millisecond, "Normal transaction not included") |
| 151 | + t.Log("Normal transaction included") |
| 152 | + |
| 153 | + // 2. Send a Forced Inclusion transaction |
| 154 | + t.Log("Sending forced inclusion transaction...") |
| 155 | + txForce := evm.GetRandomTransaction(t, TestPrivateKey, TestToAddress, DefaultChainID, DefaultGasLimit, &nonce) |
| 156 | + txBytes, err := txForce.MarshalBinary() |
| 157 | + require.NoError(t, err) |
| 158 | + |
| 159 | + submitForceInclusionTx(t, fiUrl, txBytes) |
| 160 | + t.Logf("Forced inclusion transaction submitted: %s", txForce.Hash().Hex()) |
| 161 | + |
| 162 | + // Wait for inclusion |
| 163 | + // Force inclusion depends on DA epoch. With epoch=2 and fast DA block time (200ms), |
| 164 | + // this should be reasonably fast, but we allow enough time for robustness. |
| 165 | + require.Eventually(t, func() bool { |
| 166 | + return evm.CheckTxIncluded(client, txForce.Hash()) |
| 167 | + }, 30*time.Second, 1*time.Second, "Forced inclusion transaction not included") |
| 168 | + |
| 169 | + t.Log("Forced inclusion transaction included successfully in Sequencer") |
| 170 | +} |
| 171 | + |
| 172 | +func TestEvmFullNodeForceInclusionE2E(t *testing.T) { |
| 173 | + sut := NewSystemUnderTest(t) |
| 174 | + workDir := t.TempDir() |
| 175 | + sequencerHome := filepath.Join(workDir, "sequencer") |
| 176 | + fullNodeHome := filepath.Join(workDir, "fullnode") |
| 177 | + |
| 178 | + // Get a port for force inclusion server |
| 179 | + fiPort, err := getAvailablePort() |
| 180 | + require.NoError(t, err) |
| 181 | + fiUrl := fmt.Sprintf("http://127.0.0.1:%d", fiPort) |
| 182 | + |
| 183 | + // --- Start Sequencer Setup --- |
| 184 | + // We manually setup sequencer here because we need the force inclusion flag, |
| 185 | + // and we need to capture variables for full node setup. |
| 186 | + jwtSecret, fullNodeJwtSecret, genesisHash, endpoints := setupCommonEVMTest(t, sut, true) |
| 187 | + |
| 188 | + passphraseFile := createPassphraseFile(t, sequencerHome) |
| 189 | + jwtSecretFile := createJWTSecretFile(t, sequencerHome, jwtSecret) |
| 190 | + |
| 191 | + output, err := sut.RunCmd(evmSingleBinaryPath, |
| 192 | + "init", |
| 193 | + "--evnode.node.aggregator=true", |
| 194 | + "--evnode.signer.passphrase_file", passphraseFile, |
| 195 | + "--home", sequencerHome, |
| 196 | + ) |
| 197 | + require.NoError(t, err, "failed to init sequencer", output) |
| 198 | + |
| 199 | + // Set epoch to 2 for fast testing |
| 200 | + enableForceInclusionInGenesis(t, sequencerHome, 2) |
| 201 | + |
| 202 | + fiAddr := fmt.Sprintf("127.0.0.1:%d", fiPort) |
| 203 | + seqArgs := []string{ |
| 204 | + "start", |
| 205 | + "--evm.jwt-secret-file", jwtSecretFile, |
| 206 | + "--evm.genesis-hash", genesisHash, |
| 207 | + "--evnode.node.block_time", DefaultBlockTime, |
| 208 | + "--evnode.node.aggregator=true", |
| 209 | + "--evnode.signer.passphrase_file", passphraseFile, |
| 210 | + "--home", sequencerHome, |
| 211 | + "--evnode.da.block_time", DefaultDABlockTime, |
| 212 | + "--evnode.da.address", endpoints.GetDAAddress(), |
| 213 | + "--evnode.da.namespace", DefaultDANamespace, |
| 214 | + "--evnode.da.forced_inclusion_namespace", "forced-inc", |
| 215 | + "--evnode.rpc.address", endpoints.GetRollkitRPCListen(), |
| 216 | + "--evnode.p2p.listen_address", endpoints.GetRollkitP2PAddress(), |
| 217 | + "--evm.engine-url", endpoints.GetSequencerEngineURL(), |
| 218 | + "--evm.eth-url", endpoints.GetSequencerEthURL(), |
| 219 | + "--force-inclusion-server", fiAddr, |
| 220 | + } |
| 221 | + sut.ExecCmd(evmSingleBinaryPath, seqArgs...) |
| 222 | + sut.AwaitNodeUp(t, endpoints.GetRollkitRPCAddress(), NodeStartupTimeout) |
| 223 | + t.Log("Sequencer is up with force inclusion enabled") |
| 224 | + // --- End Sequencer Setup --- |
| 225 | + |
| 226 | + // --- Start Full Node Setup --- |
| 227 | + // Reuse setupFullNode helper which handles genesis copying and node startup |
| 228 | + setupFullNode(t, sut, fullNodeHome, sequencerHome, fullNodeJwtSecret, genesisHash, endpoints.GetRollkitP2PAddress(), endpoints) |
| 229 | + t.Log("Full node is up") |
| 230 | + // --- End Full Node Setup --- |
| 231 | + |
| 232 | + // Connect to clients |
| 233 | + seqClient, err := ethclient.Dial(endpoints.GetSequencerEthURL()) |
| 234 | + require.NoError(t, err) |
| 235 | + defer seqClient.Close() |
| 236 | + |
| 237 | + fnClient, err := ethclient.Dial(endpoints.GetFullNodeEthURL()) |
| 238 | + require.NoError(t, err) |
| 239 | + defer fnClient.Close() |
| 240 | + |
| 241 | + var nonce uint64 = 0 |
| 242 | + |
| 243 | + // 1. Send normal tx to sequencer |
| 244 | + t.Log("Sending normal transaction...") |
| 245 | + txNormal := evm.GetRandomTransaction(t, TestPrivateKey, TestToAddress, DefaultChainID, DefaultGasLimit, &nonce) |
| 246 | + err = seqClient.SendTransaction(context.Background(), txNormal) |
| 247 | + require.NoError(t, err) |
| 248 | + |
| 249 | + // Wait for full node to sync it |
| 250 | + require.Eventually(t, func() bool { |
| 251 | + return evm.CheckTxIncluded(fnClient, txNormal.Hash()) |
| 252 | + }, 20*time.Second, 500*time.Millisecond, "Normal tx not synced to full node") |
| 253 | + t.Log("Normal tx synced to full node") |
| 254 | + |
| 255 | + // 2. Send forced inclusion tx |
| 256 | + t.Log("Sending forced inclusion transaction...") |
| 257 | + txForce := evm.GetRandomTransaction(t, TestPrivateKey, TestToAddress, DefaultChainID, DefaultGasLimit, &nonce) |
| 258 | + txBytes, err := txForce.MarshalBinary() |
| 259 | + require.NoError(t, err) |
| 260 | + |
| 261 | + submitForceInclusionTx(t, fiUrl, txBytes) |
| 262 | + t.Logf("Forced inclusion transaction submitted: %s", txForce.Hash().Hex()) |
| 263 | + |
| 264 | + // Wait for full node to sync it |
| 265 | + require.Eventually(t, func() bool { |
| 266 | + return evm.CheckTxIncluded(fnClient, txForce.Hash()) |
| 267 | + }, 40*time.Second, 1*time.Second, "Forced inclusion tx not synced to full node") |
| 268 | + |
| 269 | + t.Log("Forced inclusion tx synced to full node successfully") |
| 270 | +} |
0 commit comments