fix(pool): exempt L1Handler txns from account-nonce ordering#592
Open
kariy wants to merge 1 commit into
Open
Conversation
L1-handler transactions carry the L1->L2 message nonce assigned by the settlement core (e.g. piltover), not a sequential account nonce. The stateful validators trait method tags any tx whose nonce is ahead of the senders account nonce as `Dependent`, and the pool drops it. For an L1-handler that is effectively always the case (the message nonce is unrelated to the target contracts account nonce), so the handler is parked forever and never executes. The inner `validate` fn already exempts L1Handler, but the outer dependent-nonce check returned first, so the exemption never applied. Move the L1Handler exemption ahead of the dependent-nonce check. Symptom: an appchain settling to a Starknet core contract never executes its L1->L2 messages (e.g. the cross-chain-dungeon `mint_run`): WARN "Failed to add L1Handler transaction to pool ... Invalid transaction nonce ... Account nonce: 0x0; got: 0xc", the appchain freezes and the message is never consumed. Add a regression test asserting an L1Handler whose message nonce is ahead of the target account nonce validates as Valid (not Dependent). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codec benchmark diff vs
|
| Benchmark | Baseline (ns) | Current (ns) | Δ |
|---|---|---|---|
CompiledClass(fixture)/compress |
2588500 | 2581349 | -0.28% |
CompiledClass(fixture)/decompress |
2967985 | 2926732 | -1.39% |
ExecutionCheckpoint/compress |
36 | 35 | -2.78% |
ExecutionCheckpoint/decompress |
27 | 27 | +0.00% |
PruningCheckpoint/compress |
36 | 35 | -2.78% |
PruningCheckpoint/decompress |
26 | 27 | +3.85% |
VersionedHeader/compress |
658 | 668 | +1.52% |
VersionedHeader/decompress |
895 | 885 | -1.12% |
StoredBlockBodyIndices/compress |
83 | 82 | -1.20% |
StoredBlockBodyIndices/decompress |
40 | 40 | +0.00% |
StorageEntry/compress |
164 | 167 | +1.83% |
StorageEntry/decompress |
158 | 158 | +0.00% |
ContractNonceChange/compress |
161 | 168 | +4.35% |
ContractNonceChange/decompress |
262 | 263 | +0.38% |
ContractClassChange/compress |
212 | 228 | +7.55% |
ContractClassChange/decompress |
310 | 286 | -7.74% |
ContractStorageEntry/compress |
170 | 172 | +1.18% |
ContractStorageEntry/decompress |
347 | 365 | +5.19% |
GenericContractInfo/compress |
142 | 138 | -2.82% |
GenericContractInfo/decompress |
108 | 137 | +26.85% |
Felt/compress |
94 | 92 | -2.13% |
Felt/decompress |
66 | 63 | -4.55% |
BlockHash/compress |
94 | 92 | -2.13% |
BlockHash/decompress |
64 | 63 | -1.56% |
TxHash/compress |
94 | 92 | -2.13% |
TxHash/decompress |
66 | 63 | -4.55% |
ClassHash/compress |
94 | 92 | -2.13% |
ClassHash/decompress |
66 | 61 | -7.58% |
CompiledClassHash/compress |
94 | 92 | -2.13% |
CompiledClassHash/decompress |
66 | 62 | -6.06% |
BlockNumber/compress |
50 | 51 | +2.00% |
BlockNumber/decompress |
26 | 26 | +0.00% |
TxNumber/compress |
50 | 51 | +2.00% |
TxNumber/decompress |
26 | 26 | +0.00% |
FinalityStatus/compress |
0 | 0 | NaN% |
FinalityStatus/decompress |
12 | 14 | +16.67% |
TypedTransactionExecutionInfo/compress |
14743 | 14809 | +0.45% |
TypedTransactionExecutionInfo/decompress |
3645 | 3735 | +2.47% |
VersionedContractClass/compress |
361 | 363 | +0.55% |
VersionedContractClass/decompress |
833 | 831 | -0.24% |
MigratedCompiledClassHash/compress |
166 | 167 | +0.60% |
MigratedCompiledClassHash/decompress |
159 | 157 | -1.26% |
ContractInfoChangeList/compress |
1578 | 1564 | -0.89% |
ContractInfoChangeList/decompress |
2343 | 2340 | -0.13% |
BlockChangeList/compress |
679 | 676 | -0.44% |
BlockChangeList/decompress |
951 | 960 | +0.95% |
ReceiptEnvelope/compress |
27323 | 26899 | -1.55% |
ReceiptEnvelope/decompress |
6699 | 6560 | -2.07% |
TrieDatabaseValue/compress |
163 | 160 | -1.84% |
TrieDatabaseValue/decompress |
237 | 240 | +1.27% |
TrieHistoryEntry/compress |
296 | 296 | +0.00% |
TrieHistoryEntry/decompress |
283 | 278 | -1.77% |
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #592 +/- ##
==========================================
- Coverage 73.32% 68.72% -4.60%
==========================================
Files 209 322 +113
Lines 23132 45698 +22566
==========================================
+ Hits 16961 31408 +14447
- Misses 6171 14290 +8119 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
L1-handler transactions carry the L1→L2 message nonce assigned by the settlement core (e.g. piltover /
StarknetMessaging), not a sequential account nonce. ButTxValidator's traitvalidatemethod (crates/pool/pool/src/validation/stateful.rs) runs a dependent-nonce check on every transaction:For an L1-handler,
tx.sender()is the target contract andtx.nonce()is the message nonce — which is unrelated to that contract's account nonce, and effectively always ahead of it. So the handler is taggedDependent,Pool::add_transactionmaps that toInvalidNonce, and the tx is dropped from the pool forever (it "will retry on next gather", but the gap never closes). The handler never executes.The inner
validatefn already exempts L1-handlers (Transaction::L1Handler(_) => Ok(Valid(..))), but the outer dependent-nonce check returns first, so that exemption is never reached.Symptom (how this was found)
An appchain settling to a Starknet core contract never executes its L1→L2 messages. In the
cross-chain-dungeonexample, everyEntry.enteremits amint_runL1→L2 message that katana ingests and then rejects:The appchain stays frozen (no
mint_runever runs), so the run is never created and the client hangs on "entering the dungeon" indefinitely.Fix
Move the
L1Handlerexemption ahead of the dependent-nonce check, so L1-handlers bypass account-nonce ordering entirely (consistent with the existing exemption in the innervalidate). L1-handler replay protection is the L1 message consumption, not a sequential nonce, so dropping the ordering is semantically correct.Test
Adds
l1_handler_with_nonce_gap_is_valid: builds aTxValidatorover an empty state and validates anL1HandlerTxwhose message nonce (12) is ahead of the target contract's account nonce (0). It asserts the outcome isValid.Verified locally:
FAILED(L1Handler must bypass account-nonce ordering (was tagged Dependent before the fix))ok🤖 Generated with Claude Code