Skip to content

feat: reject out-of-range checkpoint header fields at propose (A-1254) - #24199

Merged
just-mitch merged 1 commit into
mitch/gk-722-protect-fees-against-unsound-verifier-v5from
spl/a-1254-l1-field-range-checks
Jun 19, 2026
Merged

feat: reject out-of-range checkpoint header fields at propose (A-1254)#24199
just-mitch merged 1 commit into
mitch/gk-722-protect-fees-against-unsound-verifier-v5from
spl/a-1254-l1-field-range-checks

Conversation

@spalladino

@spalladino spalladino commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Part of A-1254.

A malicious proposer can currently store checkpoint header or archive field values that are >= the BN254 scalar field modulus (Constants.P) on L1. Off-chain archivers decode those storage slots into Fr elements, so an out-of-range value bricks honest archivers' L1 sync. This is the L1 half of the fix: reject such values at write time so they can never reach L1 storage.

Changes

A new helper FieldLib.requireValidFieldElement(bytes32) wraps the value < Constants.P check and reverts with Errors.Rollup__FieldElementOutOfRange(bytes32 value). It is used to range-check the attacker-controlled, Fr-decoded values that are not otherwise constrained:

  • ProposeLib.validateHeader — header fields blockHeadersHash, outHash, feeRecipient, accumulatedFees.
  • ProposeLib.propose — the new checkpoint archive root (_args.archive), which is not part of the header, checked before the digest/header validation/storage.
  • STFLib.initialize — the genesis archive root, which is written to archives[0] at deployment and propagates into the first header's lastArchiveRoot.

Why the other fields are already safe

  • lastArchiveRoot, inHash, blobsHash — equality-constrained to on-chain values that are already field-reduced (tipArchive, inbox.consume(), blobsHashesCommitment).
  • totalManaUsed — bounded by <= FeeLib.getManaLimit(), and the mana limit is capped at type(uint32).max, far below P.
  • gasFees (uint128), coinbase (address), slotNumber / timestamp — type-bounded or equality-checked against slot-derived values.

Tests

New test/RollupFieldRange.t.sol:

  • Each of the five guarded propose fields reverts with Rollup__FieldElementOutOfRange when set to Constants.P (boundary) and to type(uint256).max.
  • The genesis archive root reverts at deploy time when out of range.
  • An otherwise-valid header with every guarded field at Constants.P - 1 proposes successfully, confirming the boundary is exclusive and legitimate field elements are not rejected.

Verified red/green for both the propose checks and the genesis check; the broader RollupTest, escape-hatch, and fees suites remain green.

Follow-up

The archiver-side defense-in-depth (decoding out-of-range values without bricking sync) is a separate follow-up PR that will close A-1254.

@spalladino
spalladino requested a review from just-mitch as a code owner June 19, 2026 15:34
@spalladino
spalladino force-pushed the spl/a-1254-l1-field-range-checks branch from d2e8d4f to b30ce6e Compare June 19, 2026 15:53
A malicious proposer could store header or archive field values >= the BN254
scalar field modulus on L1. Off-chain archivers decode those storage slots into
Fr elements, so an out-of-range value bricks honest archivers' L1 sync.

Reject such values at propose time:
- validateHeader: blockHeadersHash, outHash, feeRecipient, accumulatedFees
- propose: the new checkpoint archive root

Other header fields are already safe (equality-checked against field-reduced
on-chain values, or type-bounded), so they are left unchecked.

This is the L1 half of A-1254; the archiver-side defense-in-depth is a separate
follow-up PR.
@spalladino
spalladino force-pushed the spl/a-1254-l1-field-range-checks branch from b30ce6e to 310c8ac Compare June 19, 2026 15:58
* (`type(uint256).max`), and that an otherwise-valid header with every checked field at `P - 1` still goes through.
*/
contract RollupFieldRangeTest is RollupBase {
uint256 internal constant FIELD_MAX = Constants.P - 1;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not worth fixing, just funny: I sanity checked Constants.P, and turns out there is a Constants.MAX_FIELD_VALUE.

@just-mitch
just-mitch merged commit ac27fa0 into mitch/gk-722-protect-fees-against-unsound-verifier-v5 Jun 19, 2026
7 of 9 checks passed
@just-mitch
just-mitch deleted the spl/a-1254-l1-field-range-checks branch June 19, 2026 16:05
spalladino added a commit that referenced this pull request Jun 19, 2026
…ue ids

Rebuild the out-of-range checkpoint-header e2e to exercise full recovery:
a lone proposer lands an under-attested checkpoint, its stored archive root
is corrupted to a value above the BN254 modulus via setStorageAt, and the
honest validators plus an archiver-only observer keep syncing through the
hardened status()/archiveAt() reads, invalidate the under-attested
checkpoint, and build past it. The corruption is injected post-propose
because the L1 propose path now reverts on out-of-range fields (#24199).

Also strip Linear issue-id mentions from the decode-robustness comments
across archiver, ethereum, stdlib, and sequencer-client, keeping the
technical rationale and the #24199 PR cross-reference.
spalladino added a commit that referenced this pull request Jun 19, 2026
…-of-range root

RollupContract.getCheckpoint eagerly converted the on-chain archive root to Fr, which
throws when the stored value is outside the BN254 field. A malicious proposer can land
such a value, bricking any honest node that reads the checkpoint on a sync/startup path
(e.g. the tx-pool fee provider booting against the pending tip). Carry the archive as
Buffer32 on the read path and compare archive roots as bytes in the prover-node publisher
so a mismatching out-of-range value reports a mismatch instead of throwing.

Refs #24199
rangozd pushed a commit to rangozd/aztec-packages that referenced this pull request Aug 5, 2026
…-range-next

feat: reject out-of-range checkpoint header fields at propose (port of AztecProtocol#24199 to next)
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.

2 participants