Skip to content

Commit d4e35f9

Browse files
author
SharovBot
committed
[SharovBot] fix(txpool): emit state-diff entries for AuRa system-call senders to evict stale pending txns
On AuRa/Gnosis Chain, block finalization (engine.Finalize) executes system transactions (validator rewards, bridge calls, etc.) that advance sender nonces. These state changes were NOT reaching the txpool state-diff batch because the block-end stateWriter in exec3_serial.go was constructed with a nil accumulator. Fix: store the block's accumulator on serialExecutor (se.accumulator) and pass it to the block-end stateWriter so that UpdateAccountData → ChangeAccount calls during Finalize/FinalizeAndAssemble emit UPSERT entries into the batch. The txpool then calls onSenderStateChange for those senders and evicts any pending transactions whose nonces are now stale. Changes: - execution/stagedsync/exec3_serial.go: - Add accumulator field to serialExecutor - Set se.accumulator = accumulator alongside StartChange per block - Pass se.accumulator to state.NewWriter in the block-end path - txnprovider/txpool/pool.go: - Remove txpool-level ensureMinedSendersInStateDiff workaround - Restore original cache.OnNewBlock ordering - txnprovider/txpool/pool_test.go: - Update TestStalePendingEvictionViaMineNonce to test via correct stateChanges from the EL (as now emitted after this fix) This is the EL-level fix requested by @yperbasis on #19392/#19393.
1 parent 58dba7c commit d4e35f9

File tree

4 files changed

+134
-4
lines changed

4 files changed

+134
-4
lines changed

execution/stagedsync/exec3_parallel.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,16 @@ type parallelExecutor struct {
9090
rws *exec.ResultsQueue
9191
workerCount int
9292
blockExecutors map[uint64]*blockExecutor
93+
accumulator *shards.Accumulator
9394
}
9495

9596
func (pe *parallelExecutor) exec(ctx context.Context, execStage *StageState, u Unwinder,
9697
startBlockNum uint64, offsetFromBlockBeginning uint64, maxBlockNum uint64, blockLimit uint64,
9798
initialTxNum uint64, inputTxNum uint64, initialCycle bool, rwTx kv.TemporalRwTx,
9899
stepsInDb float64, accumulator *shards.Accumulator, readAhead chan uint64, logEvery *time.Ticker) (*types.Header, kv.TemporalRwTx, error) {
99100

101+
pe.accumulator = accumulator
102+
100103
var asyncTxChan mdbx.TxApplyChan
101104
var asyncTx kv.TemporalTx
102105

@@ -695,7 +698,7 @@ func (pe *parallelExecutor) execLoop(ctx context.Context) (err error) {
695698
blockExecutor.versionMap.FlushVersionedWrites(finalWrites, true, "")
696699
}
697700

698-
stateWriter := state.NewBufferedWriter(pe.rs, nil)
701+
stateWriter := state.NewBufferedWriter(pe.rs, pe.accumulator)
699702
if err = ibs.MakeWriteSet(txTask.EvmBlockContext.Rules(txTask.Config), stateWriter); err != nil {
700703
return state.StateUpdates{}, err
701704
}
@@ -1559,7 +1562,7 @@ func (be *blockExecutor) nextResult(ctx context.Context, pe *parallelExecutor, r
15591562
}
15601563
}
15611564

1562-
stateWriter := state.NewBufferedWriter(pe.rs, nil)
1565+
stateWriter := state.NewBufferedWriter(pe.rs, pe.accumulator)
15631566

15641567
_, addReads, addWrites, err := txResult.finalize(prevReceipt, pe.cfg.engine, be.versionMap, stateReader, stateWriter)
15651568

execution/stagedsync/exec3_serial.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ type serialExecutor struct {
3535
blobGasUsed uint64
3636
lastBlockResult *blockResult
3737
worker *exec.Worker
38+
39+
// accumulator for the current block; set at StartChange and used by the
40+
// block-end stateWriter so that AuRa system-call nonce changes are
41+
// included in the txpool state-diff batch.
42+
accumulator *shards.Accumulator
3843
}
3944

4045
func warmTxsHashes(block *types.Block) {
@@ -124,6 +129,7 @@ func (se *serialExecutor) exec(ctx context.Context, execStage *StageState, u Unw
124129
return se.getHeader(ctx, hash, number)
125130
}), se.cfg.engine, se.cfg.author, se.cfg.chainConfig)
126131

132+
se.accumulator = accumulator // keep in sync for executeBlock's stateWriter
127133
if accumulator != nil {
128134
txs, err := se.cfg.blockReader.RawTransactions(context.Background(), se.applyTx, b.NumberU64(), b.NumberU64())
129135
if err != nil {
@@ -402,7 +408,10 @@ func (se *serialExecutor) executeBlock(ctx context.Context, tasks []exec.Task, i
402408
}
403409
}
404410

405-
stateWriter := state.NewWriter(se.doms.AsPutDel(se.applyTx), nil, txTask.TxNum)
411+
// Pass se.accumulator so that AuRa / system-call nonce changes
412+
// are included in the txpool state-diff batch (fixes empty block
413+
// production on Gnosis Chain caused by stale pending txns).
414+
stateWriter := state.NewWriter(se.doms.AsPutDel(se.applyTx), se.accumulator, txTask.TxNum)
406415

407416
if err = ibs.MakeWriteSet(txTask.Rules(), stateWriter); err != nil {
408417
panic(err)

txnprovider/txpool/pool.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,6 @@ func (p *TxPool) OnNewBlock(ctx context.Context, stateChanges *remoteproto.State
335335
if err != nil {
336336
return err
337337
}
338-
339338
defer coreTx.Rollback()
340339

341340
block := stateChanges.ChangeBatch[len(stateChanges.ChangeBatch)-1].BlockHeight

txnprovider/txpool/pool_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1979,3 +1979,122 @@ func TestZombieQueuedEviction(t *testing.T) {
19791979
assert.Equal(count, pending2, "all consecutive txns should be pending (no gaps, sufficient balance)")
19801980
})
19811981
}
1982+
1983+
// TestStalePendingEvictionViaMineNonce verifies that when the execution layer
1984+
// correctly emits a state-diff UPSERT for an AuRa/Gnosis system-transaction
1985+
// sender (fixed in exec3_serial.go by passing the accumulator to the block-end
1986+
// stateWriter), the txpool evicts the now-stale pending transactions.
1987+
//
1988+
// Scenario:
1989+
// 1. addr1 has two pending txns: T1 (nonce=0) and T2 (nonce=1).
1990+
// 2. The block mines T1 from the pool AND an AuRa system tx at nonce=1,
1991+
// advancing the on-chain nonce to 2. Only T1 appears in minedTxns.
1992+
// 3. The EL (after the exec3_serial.go fix) emits addr1 with nonce=2 in
1993+
// stateChanges. The pool receives this via OnNewBlock.
1994+
// 4. removeMined removes T1; onSenderStateChange(nonce=2) evicts T2 (nonce=1).
1995+
func TestStalePendingEvictionViaMineNonce(t *testing.T) {
1996+
asrt := assert.New(t)
1997+
req := require.New(t)
1998+
logger := log.New()
1999+
2000+
ctx, cancel := context.WithCancel(context.Background())
2001+
t.Cleanup(cancel)
2002+
2003+
ch := make(chan Announcements, 100)
2004+
coreDB := temporaltest.NewTestDB(t, datadir.New(t.TempDir()))
2005+
cfg := txpoolcfg.DefaultConfig
2006+
2007+
// DummyCache reads directly from the DB — avoids coherence-version coupling.
2008+
pool, err := New(ctx, ch, nil, coreDB, cfg, kvcache.NewDummy(), chain.TestChainConfig, nil, nil, func() {}, nil, nil, logger, WithFeeCalculator(nil))
2009+
req.NoError(err)
2010+
req.NotNil(pool)
2011+
2012+
var addr1 [20]byte
2013+
addr1[0] = 1
2014+
h0 := gointerfaces.ConvertHashToH256([32]byte{})
2015+
2016+
// writeAccount writes addr1 to coreDB at the given nonce so that senders.info
2017+
// (which reads from the DB when using DummyCache) returns the expected value.
2018+
writeAccount := func(nonce, txNum uint64) {
2019+
tx, werr := coreDB.BeginTemporalRw(ctx)
2020+
req.NoError(werr)
2021+
defer tx.Rollback()
2022+
sd, werr := execctx.NewSharedDomains(ctx, tx, logger)
2023+
req.NoError(werr)
2024+
a := accounts3.Account{
2025+
Nonce: nonce, Balance: *uint256.NewInt(1 * common.Ether),
2026+
CodeHash: accounts.EmptyCodeHash, Incarnation: 1,
2027+
}
2028+
req.NoError(sd.DomainPut(kv.AccountsDomain, tx, addr1[:], accounts3.SerialiseV3(&a), txNum, nil))
2029+
req.NoError(sd.Flush(ctx, tx))
2030+
sd.Close()
2031+
req.NoError(tx.Commit())
2032+
}
2033+
2034+
serialiseAcc := func(nonce uint64) []byte {
2035+
a := accounts3.Account{
2036+
Nonce: nonce, Balance: *uint256.NewInt(1 * common.Ether),
2037+
CodeHash: accounts.EmptyCodeHash, Incarnation: 1,
2038+
}
2039+
return accounts3.SerialiseV3(&a)
2040+
}
2041+
2042+
// ── Step 1: write addr1 nonce=0 to DB and bootstrap pool ─────────────────
2043+
writeAccount(0, 0)
2044+
initChange := &remoteproto.StateChangeBatch{
2045+
StateVersionId: 0, PendingBlockBaseFee: 200_000, BlockGasLimit: 1_000_000,
2046+
ChangeBatch: []*remoteproto.StateChange{{BlockHeight: 0, BlockHash: h0}},
2047+
}
2048+
initChange.ChangeBatch[0].Changes = append(initChange.ChangeBatch[0].Changes, &remoteproto.AccountChange{
2049+
Action: remoteproto.Action_UPSERT,
2050+
Address: gointerfaces.ConvertAddressToH160(addr1),
2051+
Data: serialiseAcc(0),
2052+
})
2053+
req.NoError(pool.OnNewBlock(ctx, initChange, TxnSlots{}, TxnSlots{}, TxnSlots{}))
2054+
2055+
// ── Step 2: add T1 (nonce=0) and T2 (nonce=1) to pending ────────────────
2056+
T1 := &TxnSlot{Tip: *uint256.NewInt(300_000), FeeCap: *uint256.NewInt(300_000), Gas: 100_000, Nonce: 0}
2057+
T1.IDHash[0] = 1
2058+
T2 := &TxnSlot{Tip: *uint256.NewInt(300_000), FeeCap: *uint256.NewInt(300_000), Gas: 100_000, Nonce: 1}
2059+
T2.IDHash[0] = 2
2060+
var slots TxnSlots
2061+
slots.Append(T1, addr1[:], true)
2062+
slots.Append(T2, addr1[:], true)
2063+
reasons, err := pool.AddLocalTxns(ctx, slots)
2064+
req.NoError(err)
2065+
for _, r := range reasons {
2066+
asrt.Equal(txpoolcfg.Success, r, r.String())
2067+
}
2068+
pending, _, _ := pool.CountContent()
2069+
asrt.Equal(2, pending, "both T1 and T2 should be in pending")
2070+
2071+
// ── Step 3: advance DB nonce to 2 (T1 mined + AuRa system tx at nonce=1) ─
2072+
writeAccount(2, 1)
2073+
2074+
// ── Step 4: OnNewBlock with the correct stateChanges from the fixed EL ───
2075+
// The exec3_serial.go fix ensures addr1 appears in stateChanges with
2076+
// nonce=2 because the block-end stateWriter now carries the accumulator.
2077+
h1 := gointerfaces.ConvertHashToH256([32]byte{1})
2078+
blockChange := &remoteproto.StateChangeBatch{
2079+
StateVersionId: 1, PendingBlockBaseFee: 200_000, BlockGasLimit: 1_000_000,
2080+
ChangeBatch: []*remoteproto.StateChange{{BlockHeight: 1, BlockHash: h1}},
2081+
}
2082+
blockChange.ChangeBatch[0].Changes = append(blockChange.ChangeBatch[0].Changes, &remoteproto.AccountChange{
2083+
Action: remoteproto.Action_UPSERT,
2084+
Address: gointerfaces.ConvertAddressToH160(addr1),
2085+
Data: serialiseAcc(2), // EL now correctly emits nonce=2
2086+
})
2087+
2088+
minedT1 := &TxnSlot{Tip: *uint256.NewInt(300_000), FeeCap: *uint256.NewInt(300_000), Gas: 100_000, Nonce: 0}
2089+
minedT1.IDHash[0] = 1
2090+
var minedSlots TxnSlots
2091+
minedSlots.Append(minedT1, addr1[:], true)
2092+
2093+
req.NoError(pool.OnNewBlock(ctx, blockChange, TxnSlots{}, TxnSlots{}, minedSlots))
2094+
2095+
// ── Step 5: T1 removed by removeMined; T2 evicted by onSenderStateChange ─
2096+
// senderNonce=2 (from DB) > T2.Nonce=1 → NonceTooLow.
2097+
pending, _, queued := pool.CountContent()
2098+
asrt.Equal(0, pending, "T2 must be evicted: on-chain nonce=2 > T2.nonce=1")
2099+
asrt.Equal(0, queued, "no queued txns expected")
2100+
}

0 commit comments

Comments
 (0)