fix(pool): don't gate L1-handler txs on account nonce - #623
Merged
Conversation
The pool validated the settlement chain's global L1->L2 message nonce as if it were a per-target-contract sequential account nonce. Since the pool has no future/queued tier, the first non-contiguous per-target nonce — a second target contract, a node restart wiping the in-memory `pool_nonces`, or a single dropped message — was rejected as InvalidNonce and dropped, and so was every later message, permanently. The L1-handler nonce is not an account nonce: it is global/monotonic across all messages, the executor never increments the target's nonce, and blockifier treats it as metadata (matching the official sequencer, which keeps L1-handlers out of the account-nonce mempool). Short-circuit L1-handler txs to Valid before the nonce-dependency gate, leaving `pool_nonces` untouched. Adds pool unit tests (gapped / interleaved targets / restart, plus an account-tx nonce-gap regression) and extends the messaging e2e with a second target contract (verified to fail without this fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.30.
| Benchmark suite | Current: cd83e06 | Previous: c7b2907 | Ratio |
|---|---|---|---|
FinalityStatus/compress |
1 ns/iter (± 0) |
0 ns/iter (± 0) |
+∞ |
This comment was automatically generated by workflow using github-action-benchmark.
CC: @kariy
Codec benchmark diff vs
|
| Benchmark | Baseline (ns) | Current (ns) | Δ |
|---|---|---|---|
CompiledClass(fixture)/compress |
2650502 | 2653796 | +0.12% |
CompiledClass(fixture)/decompress |
2798562 | 2788567 | -0.36% |
ExecutionCheckpoint/compress |
33 | 33 | +0.00% |
ExecutionCheckpoint/decompress |
25 | 25 | +0.00% |
PruningCheckpoint/compress |
33 | 33 | +0.00% |
PruningCheckpoint/decompress |
25 | 25 | +0.00% |
VersionedHeader/compress |
631 | 631 | +0.00% |
VersionedHeader/decompress |
887 | 899 | +1.35% |
StoredBlockBodyIndices/compress |
78 | 78 | +0.00% |
StoredBlockBodyIndices/decompress |
38 | 37 | -2.63% |
StorageEntry/compress |
155 | 161 | +3.87% |
StorageEntry/decompress |
156 | 153 | -1.92% |
ContractNonceChange/compress |
172 | 163 | -5.23% |
ContractNonceChange/decompress |
264 | 268 | +1.52% |
ContractClassChange/compress |
201 | 201 | +0.00% |
ContractClassChange/decompress |
307 | 310 | +0.98% |
ContractStorageEntry/compress |
169 | 169 | +0.00% |
ContractStorageEntry/decompress |
371 | 370 | -0.27% |
GenericContractInfo/compress |
130 | 130 | +0.00% |
GenericContractInfo/decompress |
108 | 109 | +0.93% |
Felt/compress |
89 | 89 | +0.00% |
Felt/decompress |
62 | 61 | -1.61% |
BlockHash/compress |
89 | 89 | +0.00% |
BlockHash/decompress |
62 | 63 | +1.61% |
TxHash/compress |
89 | 88 | -1.12% |
TxHash/decompress |
62 | 62 | +0.00% |
ClassHash/compress |
89 | 88 | -1.12% |
ClassHash/decompress |
62 | 63 | +1.61% |
CompiledClassHash/compress |
89 | 88 | -1.12% |
CompiledClassHash/decompress |
62 | 63 | +1.61% |
BlockNumber/compress |
51 | 50 | -1.96% |
BlockNumber/decompress |
24 | 24 | +0.00% |
TxNumber/compress |
51 | 50 | -1.96% |
TxNumber/decompress |
25 | 24 | -4.00% |
FinalityStatus/compress |
0 | 0 | NaN% |
FinalityStatus/decompress |
10 | 10 | +0.00% |
TypedTransactionExecutionInfo/compress |
14377 | 14402 | +0.17% |
TypedTransactionExecutionInfo/decompress |
3737 | 3726 | -0.29% |
VersionedContractClass/compress |
381 | 390 | +2.36% |
VersionedContractClass/decompress |
839 | 827 | -1.43% |
MigratedCompiledClassHash/compress |
161 | 160 | -0.62% |
MigratedCompiledClassHash/decompress |
158 | 156 | -1.27% |
ContractInfoChangeList/compress |
1414 | 1411 | -0.21% |
ContractInfoChangeList/decompress |
2297 | 2430 | +5.79% |
BlockChangeList/compress |
616 | 620 | +0.65% |
BlockChangeList/decompress |
918 | 949 | +3.38% |
ReceiptEnvelope/compress |
26835 | 26904 | +0.26% |
ReceiptEnvelope/decompress |
6454 | 6478 | +0.37% |
TrieDatabaseValue/compress |
152 | 148 | -2.63% |
TrieDatabaseValue/decompress |
263 | 263 | +0.00% |
TrieHistoryEntry/compress |
271 | 270 | -0.37% |
TrieHistoryEntry/decompress |
309 | 307 | -0.65% |
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #623 +/- ##
==========================================
- Coverage 73.32% 68.14% -5.18%
==========================================
Files 209 333 +124
Lines 23132 46590 +23458
==========================================
+ Hits 16961 31748 +14787
- Misses 6171 14842 +8671 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This was referenced Jul 1, 2026
kariy
added a commit
that referenced
this pull request
Jul 2, 2026
…lidates #623) (#624) ## What this adds A new example, `examples/cross-chain-arcade/` — a small "arcade" game whose whole point is that **one L1 contract sends L1→L2 messages to many *different* L2 contracts**. An **arcade** dispenser on the settlement layer ("L1") calls `play_all`, which in a *single* L1 transaction sends an L1→L2 message to **four distinct machine contracts** on the appchain ("L2") — `SLOTS`, `PINBALL`, `CLAW`, `RACER`, each a separately deployed contract with its own `insert_coin` `#[l1_handler]`. The frontend shows every machine light up; `scripts/verify.ts` asserts every machine received its coin. ## What it demonstrates This is a targeted regression demo for the L1-handler nonce fix in **#623**. The settlement chain's core contract assigns a **single, global, monotonic** nonce to every L1→L2 message, regardless of target. Before #623 the pool gated that as a **per-target account nonce**, so the moment a sender messaged a *second* contract (global nonce `> 0`, but that contract's account nonce still `0`), the message — and every later one — was rejected `InvalidNonce` and never mined. `play_all` reproduces exactly this: four messages to four distinct targets with non-contiguous per-target nonces. With the fix, all four relay; without it, only `SLOTS` would. The existing `cross-chain-game` example only ever messages a single L2 target, so it never exercised this path — hence a dedicated, minimal example. ## Scope / design - Plain Cairo + `starknet.js`, **L1→L2 only** (the direction #623 touches). No Dojo, `sozo`, Torii, TEE registry, or embedded settlement. - Uses `katana init rollup` (validity mode + dummy fact registry) to deploy a real piltover core, then boots the appchain from the generated config with `--messaging.enabled`. `up.sh`/`down.sh` bring the whole stack up/down. - Verified end-to-end against katana at current `main`: one `play_all` → all four machines credited; the `verify.ts` gate passes. ## Related - Validates #623 — fix(pool): don't gate L1-handler txs on account nonce - Known follow-ups (out of scope; not exercised here): #620, #621, #622 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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→L2 messages sent through the settlement contract (Piltover / Ethereum core) would, after a while, all start getting rejected with
InvalidNonce— the settlement chain's current message nonce diverging from what Katana's state expects.Root cause: the pool validates the settlement chain's global, monotonic L1→L2 message nonce as if it were a per-target-contract sequential account nonce (
TxId { sender: contract_address, nonce: message_nonce }), and the pool has no future/queued tier — aDependent(nonce-too-high) outcome is returned asInvalidNonceand the tx is dropped, not parked (crates/pool/pool/src/pool.rs). The expected-nonce counter (pool_nonces) is in-memory only, is never reconciled, and the executor never increments the target's account nonce.So the scheme only worked while every message targeted a single contract with a contiguous
0,1,2,…sequence and the process never restarted. The first hole — a second target contract (global nonce interleaves), a restart (wipespool_nonces), or a single dropped message — permanently rejects that message and every later one.This is not how the L1-handler nonce is meant to work: it's global/monotonic, the executor doesn't treat it as an account nonce, and blockifier treats it as metadata (no nonce check, no increment). The official Starknet sequencer keeps L1-handler txs out of the account-nonce mempool entirely and orders them FIFO from a dedicated provider.
Fix
Short-circuit L1-handler txs to
Validbefore the nonce-dependency gate inTxValidator::validate, and leavepool_noncesuntouched:Ordering and execution are unaffected — L1-handlers still sort by
(contract_address, nonce)and blockifier already skips nonce checks for them; gaps simply no longer stall.Tests
validation::stateful::tests) against the realTxValidator: gapped single-target nonces(0,2,5), interleaved two-target global nonce, high-first-nonce (restart) case, and a regression asserting account-tx nonce gaps are still gated.crates/messaging/tests/e2e.rs): extendedtest_messagingto send a message to a second target contract. Verified it times out (InvalidNonce) without this fix and passes with it.Follow-ups (silent single-message loss, out of scope here)
Filed separately — with the nonce cascade gone, these no longer stall everything, but a single message can still be silently skipped:
confirmation_depthdefaults to 0🤖 Generated with Claude Code