Skip to content

Commit 309d52e

Browse files
committed
test: enable force inclusion, re-enable fi e2e and add fi benchmarks
1 parent 9c61f18 commit 309d52e

File tree

3 files changed

+308
-18
lines changed

3 files changed

+308
-18
lines changed

execution/evm/filter_bench_test.go

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
package evm
2+
3+
import (
4+
"context"
5+
"crypto/rand"
6+
"fmt"
7+
"math/big"
8+
"testing"
9+
10+
"github.com/ethereum/go-ethereum/common"
11+
"github.com/ethereum/go-ethereum/core/types"
12+
"github.com/ethereum/go-ethereum/crypto"
13+
ds "github.com/ipfs/go-datastore"
14+
dssync "github.com/ipfs/go-datastore/sync"
15+
)
16+
17+
const (
18+
benchPrivateKey = "cece4f25ac74deb1468965160c7185e07dff413f23fcadb611b05ca37ab0a52e"
19+
benchToAddress = "0x944fDcD1c868E3cC566C78023CcB38A32cDA836E"
20+
benchChainID = "1234"
21+
)
22+
23+
// createBenchClient creates a minimal EngineClient for benchmarking FilterTxs.
24+
// It only needs the logger to be set for the FilterTxs method.
25+
func createBenchClient(b *testing.B) *EngineClient {
26+
b.Helper()
27+
baseStore := dssync.MutexWrap(ds.NewMapDatastore())
28+
store := NewEVMStore(baseStore)
29+
client := &EngineClient{
30+
store: store,
31+
}
32+
return client
33+
}
34+
35+
// generateSignedTransaction creates a valid signed Ethereum transaction for benchmarking.
36+
func generateSignedTransaction(b *testing.B, nonce uint64, gasLimit uint64) []byte {
37+
b.Helper()
38+
39+
privateKey, err := crypto.HexToECDSA(benchPrivateKey)
40+
if err != nil {
41+
b.Fatalf("failed to parse private key: %v", err)
42+
}
43+
44+
chainID, ok := new(big.Int).SetString(benchChainID, 10)
45+
if !ok {
46+
b.Fatalf("failed to parse chain ID")
47+
}
48+
49+
toAddress := common.HexToAddress(benchToAddress)
50+
txValue := big.NewInt(1000000000000000000)
51+
gasPrice := big.NewInt(30000000000)
52+
53+
data := make([]byte, 16)
54+
if _, err := rand.Read(data); err != nil {
55+
b.Fatalf("failed to generate random data: %v", err)
56+
}
57+
58+
tx := types.NewTx(&types.LegacyTx{
59+
Nonce: nonce,
60+
To: &toAddress,
61+
Value: txValue,
62+
Gas: gasLimit,
63+
GasPrice: gasPrice,
64+
Data: data,
65+
})
66+
67+
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
68+
if err != nil {
69+
b.Fatalf("failed to sign transaction: %v", err)
70+
}
71+
72+
txBytes, err := signedTx.MarshalBinary()
73+
if err != nil {
74+
b.Fatalf("failed to marshal transaction: %v", err)
75+
}
76+
77+
return txBytes
78+
}
79+
80+
// generateTransactionBatch creates a batch of signed transactions for benchmarking.
81+
func generateTransactionBatch(b *testing.B, count int, gasLimit uint64) [][]byte {
82+
b.Helper()
83+
txs := make([][]byte, count)
84+
for i := 0; i < count; i++ {
85+
txs[i] = generateSignedTransaction(b, uint64(i), gasLimit)
86+
}
87+
return txs
88+
}
89+
90+
// generateMixedTransactionBatch creates a batch with some valid and some invalid transactions.
91+
// forcedRatio is the percentage of transactions that are "forced" (could include invalid ones).
92+
func generateMixedTransactionBatch(b *testing.B, count int, gasLimit uint64, includeInvalid bool) [][]byte {
93+
b.Helper()
94+
txs := make([][]byte, count)
95+
for i := 0; i < count; i++ {
96+
if includeInvalid && i%10 == 0 {
97+
// Every 10th transaction is invalid (random garbage)
98+
txs[i] = make([]byte, 100)
99+
if _, err := rand.Read(txs[i]); err != nil {
100+
b.Fatalf("failed to generate random data: %v", err)
101+
}
102+
} else {
103+
txs[i] = generateSignedTransaction(b, uint64(i), gasLimit)
104+
}
105+
}
106+
return txs
107+
}
108+
109+
func benchName(n int) string {
110+
if n >= 1000 {
111+
return fmt.Sprintf("%dk", n/1000)
112+
}
113+
return fmt.Sprintf("%d", n)
114+
}
115+
116+
// BenchmarkFilterTxs_OnlyNormalTxs benchmarks FilterTxs when hasForceIncludedTransaction is false.
117+
// In this case, UnmarshalBinary is NOT called - mempool transactions are already validated.
118+
func BenchmarkFilterTxs_OnlyNormalTxs(b *testing.B) {
119+
client := createBenchClient(b)
120+
ctx := context.Background()
121+
122+
txCounts := []int{100, 1000, 10000}
123+
124+
for _, count := range txCounts {
125+
b.Run(benchName(count), func(b *testing.B) {
126+
txs := generateTransactionBatch(b, count, 21000)
127+
128+
b.ResetTimer()
129+
b.ReportAllocs()
130+
131+
for b.Loop() {
132+
// hasForceIncludedTransaction=false means UnmarshalBinary is skipped
133+
_, err := client.FilterTxs(ctx, txs, 0, 0, false)
134+
if err != nil {
135+
b.Fatalf("FilterTxs failed: %v", err)
136+
}
137+
}
138+
})
139+
}
140+
}
141+
142+
// BenchmarkFilterTxs_WithForcedTxs benchmarks FilterTxs when hasForceIncludedTransaction is true.
143+
// In this case, UnmarshalBinary IS called for every transaction to validate and extract gas.
144+
func BenchmarkFilterTxs_WithForcedTxs(b *testing.B) {
145+
client := createBenchClient(b)
146+
ctx := context.Background()
147+
148+
txCounts := []int{100, 1000, 10000}
149+
150+
for _, count := range txCounts {
151+
b.Run(benchName(count), func(b *testing.B) {
152+
txs := generateTransactionBatch(b, count, 21000)
153+
154+
b.ResetTimer()
155+
b.ReportAllocs()
156+
157+
for b.Loop() {
158+
// hasForceIncludedTransaction=true triggers UnmarshalBinary path
159+
_, err := client.FilterTxs(ctx, txs, 0, 0, true)
160+
if err != nil {
161+
b.Fatalf("FilterTxs failed: %v", err)
162+
}
163+
}
164+
})
165+
}
166+
}
167+
168+
// BenchmarkFilterTxs_MixedWithInvalidTxs benchmarks FilterTxs with a mix of valid and invalid transactions.
169+
// This tests the UnmarshalBinary error handling path.
170+
func BenchmarkFilterTxs_MixedWithInvalidTxs(b *testing.B) {
171+
client := createBenchClient(b)
172+
ctx := context.Background()
173+
174+
txCounts := []int{100, 1000, 10000}
175+
176+
for _, count := range txCounts {
177+
b.Run(benchName(count), func(b *testing.B) {
178+
// Generate batch with 10% invalid transactions
179+
txs := generateMixedTransactionBatch(b, count, 21000, true)
180+
181+
b.ResetTimer()
182+
b.ReportAllocs()
183+
184+
for b.Loop() {
185+
// hasForceIncludedTransaction=true triggers UnmarshalBinary path
186+
_, err := client.FilterTxs(ctx, txs, 0, 0, true)
187+
if err != nil {
188+
b.Fatalf("FilterTxs failed: %v", err)
189+
}
190+
}
191+
})
192+
}
193+
}
194+
195+
// BenchmarkFilterTxs_WithGasLimit benchmarks FilterTxs with gas limit enforcement.
196+
// This tests the full path including gas accumulation logic.
197+
func BenchmarkFilterTxs_WithGasLimit(b *testing.B) {
198+
client := createBenchClient(b)
199+
ctx := context.Background()
200+
201+
txCounts := []int{100, 1000}
202+
// Gas limit that allows roughly half the transactions
203+
maxGas := uint64(21000 * 500)
204+
205+
for _, count := range txCounts {
206+
b.Run(benchName(count), func(b *testing.B) {
207+
txs := generateTransactionBatch(b, count, 21000)
208+
209+
b.ResetTimer()
210+
b.ReportAllocs()
211+
212+
for b.Loop() {
213+
// With gas limit and forced transactions
214+
_, err := client.FilterTxs(ctx, txs, 0, maxGas, true)
215+
if err != nil {
216+
b.Fatalf("FilterTxs failed: %v", err)
217+
}
218+
}
219+
})
220+
}
221+
}
222+
223+
// BenchmarkFilterTxs_WithSizeLimit benchmarks FilterTxs with byte size limit enforcement.
224+
func BenchmarkFilterTxs_WithSizeLimit(b *testing.B) {
225+
client := createBenchClient(b)
226+
ctx := context.Background()
227+
228+
txCounts := []int{100, 1000}
229+
// Size limit that allows roughly half the transactions (~110 bytes per tx)
230+
maxBytes := uint64(110 * 500)
231+
232+
for _, count := range txCounts {
233+
b.Run(benchName(count)+"_noForced", func(b *testing.B) {
234+
txs := generateTransactionBatch(b, count, 21000)
235+
236+
b.ResetTimer()
237+
b.ReportAllocs()
238+
239+
for b.Loop() {
240+
// Without forced transactions - UnmarshalBinary skipped
241+
_, err := client.FilterTxs(ctx, txs, maxBytes, 0, false)
242+
if err != nil {
243+
b.Fatalf("FilterTxs failed: %v", err)
244+
}
245+
}
246+
})
247+
248+
b.Run(benchName(count)+"_withForced", func(b *testing.B) {
249+
txs := generateTransactionBatch(b, count, 21000)
250+
251+
b.ResetTimer()
252+
b.ReportAllocs()
253+
254+
for b.Loop() {
255+
// With forced transactions - UnmarshalBinary called
256+
_, err := client.FilterTxs(ctx, txs, maxBytes, 0, true)
257+
if err != nil {
258+
b.Fatalf("FilterTxs failed: %v", err)
259+
}
260+
}
261+
})
262+
}
263+
}
264+
265+
// BenchmarkFilterTxs_CompareUnmarshalOverhead directly compares the overhead of UnmarshalBinary.
266+
// Runs the same transaction set with and without the UnmarshalBinary path.
267+
func BenchmarkFilterTxs_CompareUnmarshalOverhead(b *testing.B) {
268+
client := createBenchClient(b)
269+
ctx := context.Background()
270+
271+
count := 1000
272+
txs := generateTransactionBatch(b, count, 21000)
273+
274+
b.Run("without_unmarshal", func(b *testing.B) {
275+
b.ResetTimer()
276+
b.ReportAllocs()
277+
278+
for b.Loop() {
279+
_, err := client.FilterTxs(ctx, txs, 0, 0, false)
280+
if err != nil {
281+
b.Fatalf("FilterTxs failed: %v", err)
282+
}
283+
}
284+
})
285+
286+
b.Run("with_unmarshal", func(b *testing.B) {
287+
b.ResetTimer()
288+
b.ReportAllocs()
289+
290+
for b.Loop() {
291+
_, err := client.FilterTxs(ctx, txs, 0, 0, true)
292+
if err != nil {
293+
b.Fatalf("FilterTxs failed: %v", err)
294+
}
295+
}
296+
})
297+
}

pkg/config/config.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -363,11 +363,9 @@ func (c *Config) Validate() error {
363363
}
364364

365365
if len(c.DA.GetForcedInclusionNamespace()) > 0 {
366-
// if err := validateNamespace(c.DA.GetForcedInclusionNamespace()); err != nil {
367-
// return fmt.Errorf("could not validate forced inclusion namespace (%s): %w", c.DA.GetForcedInclusionNamespace(), err)
368-
// }
369-
return fmt.Errorf("forced inclusion is not yet live")
370-
366+
if err := validateNamespace(c.DA.GetForcedInclusionNamespace()); err != nil {
367+
return fmt.Errorf("could not validate forced inclusion namespace (%s): %w", c.DA.GetForcedInclusionNamespace(), err)
368+
}
371369
}
372370

373371
// Validate lazy mode configuration

test/e2e/evm_force_inclusion_e2e_test.go

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,6 @@ func setupSequencerWithForceInclusion(t *testing.T, sut *SystemUnderTest, nodeHo
124124
}
125125

126126
func TestEvmSequencerForceInclusionE2E(t *testing.T) {
127-
t.Skip() // To re-enable after https://github.com/evstack/ev-node/issues/2965
128-
129127
sut := NewSystemUnderTest(t)
130128
workDir := t.TempDir()
131129
sequencerHome := filepath.Join(workDir, "sequencer")
@@ -179,8 +177,6 @@ func TestEvmSequencerForceInclusionE2E(t *testing.T) {
179177
}
180178

181179
func TestEvmFullNodeForceInclusionE2E(t *testing.T) {
182-
t.Skip() // To re-enable after https://github.com/evstack/ev-node/issues/2965
183-
184180
sut := NewSystemUnderTest(t)
185181
workDir := t.TempDir()
186182
sequencerHome := filepath.Join(workDir, "sequencer")
@@ -347,24 +343,20 @@ func setupFullNodeWithForceInclusionCheck(t *testing.T, sut *SystemUnderTest, fu
347343
// Create JWT secret file for full node
348344
jwtSecretFile := createJWTSecretFile(t, fullNodeHome, jwtSecret)
349345

350-
// Get sequencer's peer ID for P2P connection
351-
sequencerID := NodeID(t, sequencerHome)
352-
353346
// Start full node WITH forced_inclusion_namespace configured
354347
// This allows it to retrieve forced txs from DA and detect when they're missing from blocks
355348
fnArgs := []string{
356349
"start",
357350
"--evm.jwt-secret-file", jwtSecretFile,
358351
"--evm.genesis-hash", genesisHash,
359-
"--evnode.node.block_time", DefaultBlockTime,
360352
"--home", fullNodeHome,
361353
"--evnode.da.block_time", DefaultDABlockTime,
362354
"--evnode.da.address", endpoints.GetDAAddress(),
363355
"--evnode.da.namespace", DefaultDANamespace,
364356
"--evnode.da.forced_inclusion_namespace", "forced-inc", // Enables forced inclusion verification
365357
"--evnode.rpc.address", endpoints.GetFullNodeRPCListen(),
366-
"--rollkit.p2p.listen_address", endpoints.GetFullNodeP2PAddress(),
367-
"--rollkit.p2p.seeds", fmt.Sprintf("%s@%s", sequencerID, sequencerP2PAddr),
358+
"--evnode.p2p.listen_address", endpoints.GetFullNodeP2PAddress(),
359+
"--evnode.p2p.peers", sequencerP2PAddr,
368360
"--evm.engine-url", endpoints.GetFullNodeEngineURL(),
369361
"--evm.eth-url", endpoints.GetFullNodeEthURL(),
370362
}
@@ -403,12 +395,12 @@ func setupFullNodeWithForceInclusionCheck(t *testing.T, sut *SystemUnderTest, fu
403395
// Expected Outcome:
404396
// - Forced tx appears in DA but NOT in sequencer's blocks
405397
// - Sync node stops advancing its block height
406-
// - In production: sync node logs "SEQUENCER IS MALICIOUS" and exits gracefully
398+
// - In production: sync node logs "SEQUENCER IS MALICIOUS" and exits.
407399
//
408400
// Note: This test simulates the scenario by having the sequencer configured to
409401
// listen to the wrong namespace, while we submit directly to the correct namespace.
410402
func TestEvmSyncerMaliciousSequencerForceInclusionE2E(t *testing.T) {
411-
t.Skip() // To re-enable after https://github.com/evstack/ev-node/issues/2965
403+
t.Skip()
412404

413405
sut := NewSystemUnderTest(t)
414406
workDir := t.TempDir()
@@ -420,8 +412,11 @@ func TestEvmSyncerMaliciousSequencerForceInclusionE2E(t *testing.T) {
420412
t.Log("Malicious sequencer started listening to WRONG forced inclusion namespace")
421413
t.Log("NOTE: Sequencer listens to 'wrong-namespace', won't see txs on 'forced-inc'")
422414

415+
sequencerP2PAddress := getNodeP2PAddress(t, sut, sequencerHome, endpoints.RollkitRPCPort)
416+
t.Logf("Sequencer P2P address: %s", sequencerP2PAddress)
417+
423418
// Setup full node that will sync from the sequencer and verify forced inclusion
424-
setupFullNodeWithForceInclusionCheck(t, sut, fullNodeHome, sequencerHome, fullNodeJwtSecret, genesisHash, endpoints.GetRollkitP2PAddress(), endpoints)
419+
setupFullNodeWithForceInclusionCheck(t, sut, fullNodeHome, sequencerHome, fullNodeJwtSecret, genesisHash, sequencerP2PAddress, endpoints)
425420
t.Log("Full node (syncer) is up and will verify forced inclusion from DA")
426421

427422
// Connect to clients

0 commit comments

Comments
 (0)