Skip to content

Commit 2832ee4

Browse files
committed
optimize tx sanity checks
1 parent 0d98330 commit 2832ee4

9 files changed

Lines changed: 309 additions & 7 deletions

File tree

block/internal/executing/executor.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,9 @@ func (e *Executor) produceBlock() error {
338338
}
339339

340340
var (
341-
header *types.SignedHeader
342-
data *types.Data
341+
header *types.SignedHeader
342+
data *types.Data
343+
batchData *BatchData
343344
)
344345

345346
// Check if there's an already stored block at the newHeight
@@ -353,7 +354,7 @@ func (e *Executor) produceBlock() error {
353354
return fmt.Errorf("failed to get block data: %w", err)
354355
} else {
355356
// get batch from sequencer
356-
batchData, err := e.retrieveBatch(e.ctx)
357+
batchData, err = e.retrieveBatch(e.ctx)
357358
if errors.Is(err, common.ErrNoBatch) {
358359
e.logger.Debug().Msg("no batch available")
359360
return nil
@@ -381,7 +382,15 @@ func (e *Executor) produceBlock() error {
381382
}
382383
}
383384

384-
newState, err := e.applyBlock(e.ctx, header.Header, data)
385+
// Pass force-included mask through context for execution optimization
386+
// Force-included txs (from DA) MUST be validated as they're from untrusted sources
387+
// Mempool txs can skip validation as they were validated when added to mempool
388+
ctx := e.ctx
389+
if batchData != nil && batchData.Batch != nil && batchData.Batch.ForceIncludedMask != nil {
390+
ctx = coreexecutor.WithForceIncludedMask(ctx, batchData.Batch.ForceIncludedMask)
391+
}
392+
393+
newState, err := e.applyBlock(ctx, header.Header, data)
385394
if err != nil {
386395
return fmt.Errorf("failed to apply block: %w", err)
387396
}

core/execution/context.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package execution
2+
3+
import "context"
4+
5+
type forceInclusionMaskContextKey struct{}
6+
7+
// WithForceIncludedMask adds the force-included mask to the context
8+
// The mask indicates which transactions are force-included from DA (true) vs mempool (false)
9+
func WithForceIncludedMask(ctx context.Context, mask []bool) context.Context {
10+
return context.WithValue(ctx, forceInclusionMaskContextKey{}, mask)
11+
}
12+
13+
// GetForceIncludedMask retrieves the force-included mask from the context
14+
// Returns nil if no mask is present in the context
15+
func GetForceIncludedMask(ctx context.Context) []bool {
16+
if mask, ok := ctx.Value(forceInclusionMaskContextKey{}).([]bool); ok {
17+
return mask
18+
}
19+
return nil
20+
}

core/sequencer/sequencing.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ type Sequencer interface {
3939
// Batch is a collection of transactions
4040
type Batch struct {
4141
Transactions [][]byte
42+
// ForceIncludedMask indicates which transactions are force-included from DA
43+
// If nil, all transactions should be validated (backward compatibility)
44+
// If set, ForceIncludedMask[i] == true means Transactions[i] is force-included from DA
45+
// and MUST be validated (untrusted source). ForceIncludedMask[i] == false means the
46+
// transaction is from mempool and can skip validation (already validated on submission)
47+
ForceIncludedMask []bool
4248
}
4349

4450
// Hash returns the cryptographic hash of the batch

execution/evm/execution.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ func (c *EngineClient) GetTxs(ctx context.Context) ([][]byte, error) {
251251

252252
// ExecuteTxs executes the given transactions at the specified block height and timestamp
253253
func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight uint64, timestamp time.Time, prevStateRoot []byte) (updatedStateRoot []byte, maxBytes uint64, err error) {
254+
forceIncludedMask := execution.GetForceIncludedMask(ctx)
255+
254256
// Filter out invalid transactions to handle gibberish gracefully
255257
validTxs := make([]string, 0, len(txs))
256258
for i, tx := range txs {
@@ -262,6 +264,13 @@ func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight
262264
continue
263265
}
264266

267+
// Skip validation for mempool transactions (already validated when added to mempool)
268+
// Force-included transactions from DA MUST be validated as they come from untrusted sources
269+
if forceIncludedMask != nil && i < len(forceIncludedMask) && !forceIncludedMask[i] {
270+
validTxs = append(validTxs, "0x"+hex.EncodeToString(tx))
271+
continue
272+
}
273+
265274
// Validate that the transaction can be parsed as an Ethereum transaction
266275
var ethTx types.Transaction
267276
if err := ethTx.UnmarshalBinary(tx); err != nil {
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package evm
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"testing"
7+
8+
"github.com/ethereum/go-ethereum/common"
9+
"github.com/ethereum/go-ethereum/core/types"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
13+
coreexecution "github.com/evstack/ev-node/core/execution"
14+
)
15+
16+
// TestExecuteTxs_ForceIncludedMask verifies that force-included transactions
17+
// are validated while mempool transactions skip validation (already validated on submission)
18+
func TestExecuteTxs_ForceIncludedMask(t *testing.T) {
19+
t.Parallel()
20+
21+
// Create a valid Ethereum transaction for testing
22+
validTx := types.NewTransaction(
23+
0, // nonce
24+
common.HexToAddress("0x1234567890123456789012345678901234567890"), // to
25+
common.Big0, // value
26+
21000, // gas
27+
common.Big1, // gasPrice
28+
nil, // data
29+
)
30+
validTxBytes, err := validTx.MarshalBinary()
31+
require.NoError(t, err)
32+
33+
// Create invalid transaction bytes (gibberish)
34+
invalidTxBytes := []byte("this is not a valid transaction")
35+
36+
tests := []struct {
37+
name string
38+
txs [][]byte
39+
mask []bool
40+
expectedValidTxs int
41+
expectedFilterMsg string
42+
}{
43+
{
44+
name: "all transactions validated when no mask",
45+
txs: [][]byte{
46+
validTxBytes,
47+
validTxBytes,
48+
invalidTxBytes, // Should be filtered
49+
validTxBytes,
50+
},
51+
mask: nil, // No mask = validate all
52+
expectedValidTxs: 3, // 3 valid, 1 invalid filtered
53+
},
54+
{
55+
name: "force-included invalid tx must be validated (filtered out)",
56+
txs: [][]byte{
57+
invalidTxBytes, // Force-included, MUST be validated - will be filtered
58+
validTxBytes, // Mempool tx, skips validation
59+
},
60+
mask: []bool{true, false},
61+
expectedValidTxs: 1, // Only mempool tx passes (force-included filtered)
62+
},
63+
{
64+
name: "mixed force-included and mempool transactions",
65+
txs: [][]byte{
66+
validTxBytes, // Force-included, validated (valid)
67+
validTxBytes, // Force-included, validated (valid)
68+
invalidTxBytes, // Mempool tx, skips validation (passes through)
69+
validTxBytes, // Mempool tx, skips validation (passes through)
70+
},
71+
mask: []bool{true, true, false, false},
72+
expectedValidTxs: 4, // 2 valid force-included + 2 mempool txs
73+
},
74+
{
75+
name: "all force-included transactions must be validated",
76+
txs: [][]byte{
77+
invalidTxBytes, // Force-included gibberish - filtered
78+
invalidTxBytes, // Force-included gibberish - filtered
79+
invalidTxBytes, // Force-included gibberish - filtered
80+
},
81+
mask: []bool{true, true, true},
82+
expectedValidTxs: 0, // All filtered out (invalid)
83+
},
84+
{
85+
name: "all mempool transactions skip validation",
86+
txs: [][]byte{
87+
validTxBytes,
88+
validTxBytes,
89+
invalidTxBytes, // Skips validation, passes through
90+
},
91+
mask: []bool{false, false, false},
92+
expectedValidTxs: 3, // All pass (validation skipped)
93+
},
94+
{
95+
name: "empty mask same as no mask",
96+
txs: [][]byte{
97+
validTxBytes,
98+
invalidTxBytes, // Should be filtered
99+
},
100+
mask: []bool{},
101+
expectedValidTxs: 1, // 1 valid, 1 filtered
102+
},
103+
}
104+
105+
for _, tt := range tests {
106+
t.Run(tt.name, func(t *testing.T) {
107+
t.Parallel()
108+
109+
// Create context with or without mask
110+
ctx := context.Background()
111+
if tt.mask != nil {
112+
ctx = coreexecution.WithForceIncludedMask(ctx, tt.mask)
113+
}
114+
115+
// Call the validation logic by inspecting how transactions are filtered
116+
// We'll extract the logic to count valid transactions
117+
forceIncludedMask := coreexecution.GetForceIncludedMask(ctx)
118+
validTxs := make([]string, 0, len(tt.txs))
119+
skippedValidation := 0
120+
121+
for i, tx := range tt.txs {
122+
if len(tx) == 0 {
123+
continue
124+
}
125+
126+
// Skip validation for mempool transactions (already validated when added to mempool)
127+
// Force-included transactions from DA MUST be validated
128+
if forceIncludedMask != nil && i < len(forceIncludedMask) && !forceIncludedMask[i] {
129+
validTxs = append(validTxs, "0x"+hex.EncodeToString(tx))
130+
skippedValidation++
131+
continue
132+
}
133+
134+
// Validate force-included transactions (and all txs when no mask)
135+
var ethTx types.Transaction
136+
if err := ethTx.UnmarshalBinary(tx); err != nil {
137+
// Invalid transaction, skip it
138+
continue
139+
}
140+
141+
validTxs = append(validTxs, "0x"+hex.EncodeToString(tx))
142+
}
143+
144+
// Verify expected number of valid transactions
145+
assert.Equal(t, tt.expectedValidTxs, len(validTxs),
146+
"unexpected number of valid transactions")
147+
148+
// Verify mempool transactions were actually skipped
149+
if tt.mask != nil {
150+
expectedSkipped := 0
151+
for i, isForceIncluded := range tt.mask {
152+
// Skip when NOT force-included (i.e., mempool tx)
153+
if !isForceIncluded && i < len(tt.txs) && len(tt.txs[i]) > 0 {
154+
expectedSkipped++
155+
}
156+
}
157+
assert.Equal(t, expectedSkipped, skippedValidation,
158+
"unexpected number of skipped validations")
159+
}
160+
})
161+
}
162+
}
163+
164+
// TestWithForceIncludedMask_ContextRoundtrip verifies that the mask
165+
// can be stored in and retrieved from context correctly
166+
func TestWithForceIncludedMask_ContextRoundtrip(t *testing.T) {
167+
t.Parallel()
168+
169+
tests := []struct {
170+
name string
171+
mask []bool
172+
}{
173+
{
174+
name: "nil mask",
175+
mask: nil,
176+
},
177+
{
178+
name: "empty mask",
179+
mask: []bool{},
180+
},
181+
{
182+
name: "single element",
183+
mask: []bool{true},
184+
},
185+
{
186+
name: "multiple elements",
187+
mask: []bool{true, false, true, false, false},
188+
},
189+
{
190+
name: "all true",
191+
mask: []bool{true, true, true},
192+
},
193+
{
194+
name: "all false",
195+
mask: []bool{false, false, false},
196+
},
197+
}
198+
199+
for _, tt := range tests {
200+
t.Run(tt.name, func(t *testing.T) {
201+
t.Parallel()
202+
203+
ctx := context.Background()
204+
205+
// Add mask to context
206+
ctxWithMask := coreexecution.WithForceIncludedMask(ctx, tt.mask)
207+
208+
// Retrieve mask from context
209+
retrieved := coreexecution.GetForceIncludedMask(ctxWithMask)
210+
211+
// Verify it matches
212+
assert.Equal(t, tt.mask, retrieved)
213+
})
214+
}
215+
}
216+
217+
// TestGetForceIncludedMask_NoMask verifies that getting a mask from
218+
// a context without one returns nil
219+
func TestGetForceIncludedMask_NoMask(t *testing.T) {
220+
t.Parallel()
221+
222+
ctx := context.Background()
223+
mask := coreexecution.GetForceIncludedMask(ctx)
224+
225+
assert.Nil(t, mask, "expected nil mask from context without mask")
226+
}
227+
228+
// TestGetForceIncludedMask_WrongType verifies that wrong types in context
229+
// are handled gracefully
230+
func TestGetForceIncludedMask_WrongType(t *testing.T) {
231+
t.Parallel()
232+
233+
// Create context with wrong type
234+
ctx := context.WithValue(context.Background(), "force_included_mask", "wrong type")
235+
236+
mask := coreexecution.GetForceIncludedMask(ctx)
237+
238+
assert.Nil(t, mask, "expected nil mask when context has wrong type")
239+
}

execution/evm/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,5 @@ require (
5959
gopkg.in/yaml.v2 v2.4.0 // indirect
6060
gopkg.in/yaml.v3 v3.0.1 // indirect
6161
)
62+
63+
replace github.com/evstack/ev-node/core => ../../core

execution/evm/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,6 @@ github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo2
6060
github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk=
6161
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
6262
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
63-
github.com/evstack/ev-node/core v1.0.0-beta.5 h1:lgxE8XiF3U9pcFgh7xuKMgsOGvLBGRyd9kc9MR4WL0o=
64-
github.com/evstack/ev-node/core v1.0.0-beta.5/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
6563
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
6664
github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
6765
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=

sequencers/based/sequencer.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,16 @@ func (s *BasedSequencer) createBatchFromQueue(maxBytes uint64) *coresequencer.Ba
156156
}
157157
}
158158

159-
return &coresequencer.Batch{Transactions: batch}
159+
// Mark all transactions as force-included since based sequencer only pulls from DA
160+
forceIncludedMask := make([]bool, len(batch))
161+
for i := range forceIncludedMask {
162+
forceIncludedMask[i] = true
163+
}
164+
165+
return &coresequencer.Batch{
166+
Transactions: batch,
167+
ForceIncludedMask: forceIncludedMask,
168+
}
160169
}
161170

162171
// VerifyBatch verifies a batch of transactions

sequencers/single/sequencer.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,13 @@ func (c *Sequencer) GetNextBatch(ctx context.Context, req coresequencer.GetNextB
203203

204204
batch.Transactions = append(forcedTxs, trimmedBatchTxs...)
205205

206+
// Create ForceIncludedMask: true for forced txs, false for mempool txs.
207+
// Forced included txs are always first in the batch.
208+
batch.ForceIncludedMask = make([]bool, len(batch.Transactions))
209+
for i := 0; i < len(forcedTxs); i++ {
210+
batch.ForceIncludedMask[i] = true
211+
}
212+
206213
c.logger.Debug().
207214
Int("forced_tx_count", len(forcedTxs)).
208215
Int("forced_txs_size", forcedTxsSize).
@@ -211,6 +218,9 @@ func (c *Sequencer) GetNextBatch(ctx context.Context, req coresequencer.GetNextB
211218
Int("total_tx_count", len(batch.Transactions)).
212219
Int("total_size", forcedTxsSize+currentBatchSize).
213220
Msg("combined forced inclusion and batch transactions")
221+
} else if len(batch.Transactions) > 0 {
222+
// No forced txs, but we have mempool txs - mark all as non-force-included
223+
batch.ForceIncludedMask = make([]bool, len(batch.Transactions))
214224
}
215225

216226
return &coresequencer.GetNextBatchResponse{

0 commit comments

Comments
 (0)