Skip to content

fix(pool): exempt L1Handler txns from account-nonce ordering#592

Open
kariy wants to merge 1 commit into
mainfrom
fix/pool-l1-handler-nonce
Open

fix(pool): exempt L1Handler txns from account-nonce ordering#592
kariy wants to merge 1 commit into
mainfrom
fix/pool-l1-handler-nonce

Conversation

@kariy

@kariy kariy commented Jun 11, 2026

Copy link
Copy Markdown
Member

Problem

L1-handler transactions carry the L1→L2 message nonce assigned by the settlement core (e.g. piltover / StarknetMessaging), not a sequential account nonce. But TxValidator's trait validate method (crates/pool/pool/src/validation/stateful.rs) runs a dependent-nonce check on every transaction:

let tx_nonce = tx.nonce();
let address  = tx.sender();
...
let current_nonce = /* sender's account nonce, from pool or state */;
if tx_nonce > current_nonce {
    return Ok(ValidationOutcome::Dependent { current_nonce, tx_nonce, tx });
}

For an L1-handler, tx.sender() is the target contract and tx.nonce() is the message nonce — which is unrelated to that contract's account nonce, and effectively always ahead of it. So the handler is tagged Dependent, Pool::add_transaction maps that to InvalidNonce, 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 validate fn 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-dungeon example, every Entry.enter emits a mint_run L1→L2 message that katana ingests and then rejects:

INFO  messaging: L1Handler transaction added to the pool. tx_hash=0x25d367… msg_hash=0x06a3efb1…
TRACE pool: Dependent transaction. tx_nonce=12 current_nonce=0
WARN  messaging: Failed to add L1Handler transaction to pool; will retry on next gather.
      error=Invalid transaction nonce of contract at address 0x6d3d2eab…
            Account nonce: 0x0; got: 0xc.

The appchain stays frozen (no mint_run ever runs), so the run is never created and the client hangs on "entering the dungeon" indefinitely.

Fix

Move the L1Handler exemption ahead of the dependent-nonce check, so L1-handlers bypass account-nonce ordering entirely (consistent with the existing exemption in the inner validate). 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 a TxValidator over an empty state and validates an L1HandlerTx whose message nonce (12) is ahead of the target contract's account nonce (0). It asserts the outcome is Valid.

Verified locally:

  • without the fix → FAILED (L1Handler must bypass account-nonce ordering (was tagged Dependent before the fix))
  • with the fix → ok

🤖 Generated with Claude Code

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>
@github-actions

Copy link
Copy Markdown

Codec benchmark diff vs main

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%

@github-actions

Copy link
Copy Markdown

Runner: AMD EPYC 9V74 80-Core Processor (4 cores) · 15Gi RAM

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.72%. Comparing base (9bde0ae) to head (ca858cb).
⚠️ Report is 439 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant