Skip to content

fix(pool): don't gate L1-handler txs on account nonce - #623

Merged
kariy merged 2 commits into
mainfrom
fix/l1-handler-nonce-gate
Jul 1, 2026
Merged

fix(pool): don't gate L1-handler txs on account nonce#623
kariy merged 2 commits into
mainfrom
fix/l1-handler-nonce-gate

Conversation

@kariy

@kariy kariy commented Jul 1, 2026

Copy link
Copy Markdown
Member

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 — a Dependent (nonce-too-high) outcome is returned as InvalidNonce and 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 (wipes pool_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 Valid before the nonce-dependency gate in TxValidator::validate, and leave pool_nonces untouched:

if matches!(tx.transaction, ExecutableTx::L1Handler(_)) {
    return Ok(ValidationOutcome::Valid(tx));
}

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

  • Pool unit tests (validation::stateful::tests) against the real TxValidator: 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.
  • e2e (crates/messaging/tests/e2e.rs): extended test_messaging to 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:

🤖 Generated with Claude Code

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>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 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

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codec benchmark diff vs main

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%

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

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

@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

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

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.
📢 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.

@kariy
kariy merged commit bbe2576 into main Jul 1, 2026
20 of 21 checks passed
@kariy
kariy deleted the fix/l1-handler-nonce-gate branch July 1, 2026 16:52
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>
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