Skip to content

feat(l1): track which prover first proved each checkpoint - #25389

Merged
spalladino merged 8 commits into
project/v6-l1-updatesfrom
spl/first-proven-by
Sep 7, 2026
Merged

feat(l1): track which prover first proved each checkpoint#25389
spalladino merged 8 commits into
project/v6-l1-updatesfrom
spl/first-proven-by

Conversation

@spalladino

@spalladino spalladino commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Context

There was no on-chain record of who proved a given checkpoint. L2ProofVerified carries the prover, but there is nothing queryable, and the reward accounting only tracks per-epoch submissions.

Approach

Adds a firstProvenBy mapping to RollupStore, written in submitEpochRootProof whenever a proof advances the proven tip. Only the checkpoint the proof ended at gets an entry, so entries are sparse: proofs of 1-10 and then 11-20 record entries at 10 and 20, with nothing in between. Because a proof of an already proven range cannot advance the tip, it never reaches the write and the original prover is preserved.

getFirstProvenBy(checkpointNumber) resolves an arbitrary checkpoint by walking forward to the next entry. The first entry at or after the requested number belongs to the earliest proof that covered it, since the proven tip only ever advances; the walk is bounded by the epoch duration because a proof covers at most one epoch. The mapping stores the address in its lower 160 bits and sets bit 160 as a presence flag, so address(0) remains distinguishable from a sparse, unwritten entry.

The getter reverts with Rollup__CheckpointNotProven for checkpoint 0 or a checkpoint ahead of the proven tip.

The getter is routed through RewardExtLib (alongside the other STF getters) rather than inlined in Rollup.sol — the Rollup deployed bytecode is within 800 bytes of the size limit.

API changes

New IRollup.getFirstProvenBy(uint256) returns (address) and Errors.Rollup__CheckpointNotProven(uint256 proven, uint256 requested).

Fixes A-1927

Adds a `firstProvenBy` mapping to the rollup store, written whenever an epoch
proof advances the proven tip. Only the checkpoint the proof ended at gets an
entry, so entries are sparse; `getFirstProvenBy` walks forward from the
requested checkpoint to the next entry, which is bounded by the epoch duration
since a proof covers at most one epoch. A second proof of an already proven
range cannot advance the tip, so it never overwrites the original prover.
@spalladino
spalladino changed the base branch from next to project/v6-l1-updates September 4, 2026 18:57
@spalladino
spalladino marked this pull request as ready for review September 4, 2026 18:58

@LeilaWang LeilaWang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you! 🥹

solhint's ordering rule rejects a using-for declaration after a constant,
so the constant belongs with the other library constants.
RewardLib's imports and Rollup's external pure getters were already
failing solhint on the base branch.
The protocol fee margin test built a bare FeePredictor and awaited
getPredictedMinFees(), which is synchronous. That tripped
@typescript-eslint/await-thenable and failed yarn-project's
`lint --check`, and the unrefreshed predictor would have thrown at
runtime anyway. Use makeRefreshedPredictor() like the sibling
getManaMinFeeAt test does, so the margin set just above is picked up.
@AztecBot

AztecBot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Flakey Tests

🤖 says: This CI run detected 1 tests that failed, but were tolerated due to a .test_patterns.yml entry.

\033FLAKED\033 (8;;http://ci.aztec-labs.com/9e22be813ac091cd�9e22be813ac091cd8;;�): cd labs && export root= ci3= &&  yarn-project/end-to-end/scripts/run_test.sh simple src/multi-node/slashing/broadcasted_invalid_block_proposal_slash.test.ts (122s) (code: 0) group:e2e-p2p-epoch-flakes

@spalladino
spalladino merged commit 457cfb7 into project/v6-l1-updates Sep 7, 2026
9 checks passed
@spalladino
spalladino deleted the spl/first-proven-by branch September 7, 2026 14:00
spalladino added a commit that referenced this pull request Sep 10, 2026
There was no on-chain record of who proved a given checkpoint.
`L2ProofVerified` carries the prover, but there is nothing queryable,
and the reward accounting only tracks per-epoch submissions.

Adds a `firstProvenBy` mapping to `RollupStore`, written in
`submitEpochRootProof` whenever a proof advances the proven tip. Only
the checkpoint the proof ended at gets an entry, so entries are sparse:
proofs of 1-10 and then 11-20 record entries at 10 and 20, with nothing
in between. Because a proof of an already proven range cannot advance
the tip, it never reaches the write and the original prover is
preserved.

`getFirstProvenBy(checkpointNumber)` resolves an arbitrary checkpoint by
walking forward to the next entry. The first entry at or after the
requested number belongs to the earliest proof that covered it, since
the proven tip only ever advances; the walk is bounded by the epoch
duration because a proof covers at most one epoch. The mapping stores
the address in its lower 160 bits and sets bit 160 as a presence flag,
so `address(0)` remains distinguishable from a sparse, unwritten entry.

The getter reverts with `Rollup__CheckpointNotProven` for checkpoint 0
or a checkpoint ahead of the proven tip.

The getter is routed through `RewardExtLib` (alongside the other STF
getters) rather than inlined in `Rollup.sol` — the Rollup deployed
bytecode is within 800 bytes of the size limit.

New `IRollup.getFirstProvenBy(uint256) returns (address)` and
`Errors.Rollup__CheckpointNotProven(uint256 proven, uint256 requested)`.

Fixes A-1927
spalladino added a commit that referenced this pull request Sep 10, 2026
There was no on-chain record of who proved a given checkpoint.
`L2ProofVerified` carries the prover, but there is nothing queryable,
and the reward accounting only tracks per-epoch submissions.

Adds a `firstProvenBy` mapping to `RollupStore`, written in
`submitEpochRootProof` whenever a proof advances the proven tip. Only
the checkpoint the proof ended at gets an entry, so entries are sparse:
proofs of 1-10 and then 11-20 record entries at 10 and 20, with nothing
in between. Because a proof of an already proven range cannot advance
the tip, it never reaches the write and the original prover is
preserved.

`getFirstProvenBy(checkpointNumber)` resolves an arbitrary checkpoint by
walking forward to the next entry. The first entry at or after the
requested number belongs to the earliest proof that covered it, since
the proven tip only ever advances; the walk is bounded by the epoch
duration because a proof covers at most one epoch. The mapping stores
the address in its lower 160 bits and sets bit 160 as a presence flag,
so `address(0)` remains distinguishable from a sparse, unwritten entry.

The getter reverts with `Rollup__CheckpointNotProven` for checkpoint 0
or a checkpoint ahead of the proven tip.

The getter is routed through `RewardExtLib` (alongside the other STF
getters) rather than inlined in `Rollup.sol` — the Rollup deployed
bytecode is within 800 bytes of the size limit.

New `IRollup.getFirstProvenBy(uint256) returns (address)` and
`Errors.Rollup__CheckpointNotProven(uint256 proven, uint256 requested)`.

Fixes A-1927
spalladino added a commit that referenced this pull request Sep 10, 2026
There was no on-chain record of who proved a given checkpoint.
`L2ProofVerified` carries the prover, but there is nothing queryable,
and the reward accounting only tracks per-epoch submissions.

Adds a `firstProvenBy` mapping to `RollupStore`, written in
`submitEpochRootProof` whenever a proof advances the proven tip. Only
the checkpoint the proof ended at gets an entry, so entries are sparse:
proofs of 1-10 and then 11-20 record entries at 10 and 20, with nothing
in between. Because a proof of an already proven range cannot advance
the tip, it never reaches the write and the original prover is
preserved.

`getFirstProvenBy(checkpointNumber)` resolves an arbitrary checkpoint by
walking forward to the next entry. The first entry at or after the
requested number belongs to the earliest proof that covered it, since
the proven tip only ever advances; the walk is bounded by the epoch
duration because a proof covers at most one epoch. The mapping stores
the address in its lower 160 bits and sets bit 160 as a presence flag,
so `address(0)` remains distinguishable from a sparse, unwritten entry.

The getter reverts with `Rollup__CheckpointNotProven` for checkpoint 0
or a checkpoint ahead of the proven tip.

The getter is routed through `RewardExtLib` (alongside the other STF
getters) rather than inlined in `Rollup.sol` — the Rollup deployed
bytecode is within 800 bytes of the size limit.

New `IRollup.getFirstProvenBy(uint256) returns (address)` and
`Errors.Rollup__CheckpointNotProven(uint256 proven, uint256 requested)`.

Fixes A-1927
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.

3 participants