-
Notifications
You must be signed in to change notification settings - Fork 623
feat: only verify new headers #25404
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
spalladino
merged 1 commit into
stack/feat-checkpoint-reward-overrides
from
stack/feat-only-verify-new-headers
Sep 9, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -122,11 +122,17 @@ library EpochProofLib { | |
| STFLib.prune(); | ||
| } | ||
|
|
||
| (Epoch endEpoch, Epoch currentEpoch) = assertAcceptable(_args.start, _args.end); | ||
| (Epoch endEpoch, Epoch currentEpoch, uint256 provenBeforeSubmission) = assertAcceptable(_args.start, _args.end); | ||
| uint256 firstHeaderToVerify; | ||
| if (provenBeforeSubmission >= _args.start) { | ||
| uint256 provenPrefixLength = provenBeforeSubmission - _args.start + 1; | ||
| uint256 accountedPrefixLength = RewardLib.getLongestProvenLength(endEpoch); | ||
| firstHeaderToVerify = provenPrefixLength < accountedPrefixLength ? provenPrefixLength : accountedPrefixLength; | ||
| } | ||
|
|
||
| // Rehash the supplied headers against storage once, here: the public-input assembly below reads the fee | ||
| // recipient/value out of them and relies on this call having run. | ||
| verifyHeaders(_args.start, _args.end, _args.headers); | ||
| // The skipped calldata prefix is untrusted, but rewards have already consumed it and proof verification binds its | ||
| // fee data to the canonical checkpoint headers. We only verify new headers since the last proof | ||
| verifyHeaders(_args.start, _args.end, _args.headers, firstHeaderToVerify); | ||
|
|
||
| // Verify attestations for the last checkpoint in the epoch | ||
| // -> This serves as training wheels for the public part of the system (proving systems used in public and AVM) | ||
|
|
@@ -178,8 +184,8 @@ library EpochProofLib { | |
| * | ||
| * @dev The fee recipient/value public inputs are sourced from the supplied headers, so this entry point rehashes | ||
| * them against storage before assembling: an off-chain caller must not walk away with public inputs built from | ||
| * unverified fee fields and only discover the mismatch when the on-chain proof reverts. The submit path verifies | ||
| * the headers up front and assembles via computeEpochProofPublicInputs to avoid rehashing them twice. | ||
| * unverified fee fields and only discover the mismatch when the on-chain proof reverts. The submit path separately | ||
| * validates headers that have not already been proven and accounted for. | ||
| * | ||
| * @param _start - The start of the epoch (inclusive) | ||
| * @param _end - The end of the epoch (inclusive) | ||
|
|
@@ -196,7 +202,7 @@ library EpochProofLib { | |
| bytes calldata _blobPublicInputs, | ||
| RollupConfig memory _config | ||
| ) internal view returns (bytes32[] memory) { | ||
| verifyHeaders(_start, _end, _headers); | ||
| verifyHeaders(_start, _end, _headers, 0); | ||
| return computeEpochProofPublicInputs(_start, _end, _args, _headers, _blobPublicInputs, _config); | ||
| } | ||
|
|
||
|
|
@@ -416,19 +422,25 @@ library EpochProofLib { | |
| } | ||
|
|
||
| /** | ||
| * @notice Rehashes each provided checkpoint header and requires it to match the stored header hash | ||
| * @notice Rehashes a suffix of the provided checkpoint headers and requires it to match the stored header hashes | ||
| * | ||
| * @param _start The first checkpoint number in the epoch (inclusive) | ||
| * @param _end The last checkpoint number in the epoch (inclusive) | ||
| * @param _headers The proposed headers for each checkpoint in [_start, _end] | ||
| * @param _firstHeaderToVerify The index of the first header that has not already been proven and accounted for | ||
| */ | ||
| function verifyHeaders(uint256 _start, uint256 _end, ProposedHeader[] calldata _headers) private view { | ||
| function verifyHeaders( | ||
| uint256 _start, | ||
| uint256 _end, | ||
| ProposedHeader[] calldata _headers, | ||
| uint256 _firstHeaderToVerify | ||
| ) private view { | ||
| uint256 numCheckpoints = _end - _start + 1; | ||
| require( | ||
| _headers.length == numCheckpoints, Errors.Rollup__InvalidCheckpointHeaderCount(numCheckpoints, _headers.length) | ||
| ); | ||
|
|
||
| for (uint256 i = 0; i < numCheckpoints; i++) { | ||
| for (uint256 i = _firstHeaderToVerify; i < numCheckpoints; i++) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is the whole patch. |
||
| bytes32 expectedHeaderHash = STFLib.getHeaderHash(_start + i); | ||
| bytes32 providedHeaderHash = ProposedHeaderLib.hash(_headers[i]); | ||
| require( | ||
|
|
@@ -460,8 +472,13 @@ library EpochProofLib { | |
| * @param _end The last checkpoint number in the epoch (inclusive) | ||
| * @return endEpoch The epoch number that the proof covers | ||
| * @return currentEpoch The epoch at the time the proof is submitted | ||
| * @return provenBeforeSubmission The proven checkpoint number observed while checking the submission | ||
| */ | ||
| function assertAcceptable(uint256 _start, uint256 _end) private view returns (Epoch endEpoch, Epoch currentEpoch) { | ||
| function assertAcceptable(uint256 _start, uint256 _end) | ||
| private | ||
| view | ||
| returns (Epoch endEpoch, Epoch currentEpoch, uint256 provenBeforeSubmission) | ||
| { | ||
| RollupStore storage rollupStore = STFLib.getStorage(); | ||
|
|
||
| Epoch startEpoch = STFLib.getEpochForCheckpoint(_start); | ||
|
|
@@ -486,14 +503,17 @@ library EpochProofLib { | |
| bool isStartOfEpoch = _start == 1 || parentEpoch <= startEpoch - Epoch.wrap(1); | ||
| require(isStartOfEpoch, Errors.Rollup__StartIsNotFirstCheckpointOfEpoch()); | ||
|
|
||
| bool isStartBuildingOnProven = _start - 1 <= rollupStore.tips.getProven(); | ||
| provenBeforeSubmission = rollupStore.tips.getProven(); | ||
| bool isStartBuildingOnProven = _start - 1 <= provenBeforeSubmission; | ||
| require(isStartBuildingOnProven, Errors.Rollup__StartIsNotBuildingOnProven()); | ||
|
|
||
| bool claimedNumCheckpointsInEpoch = _end - _start + 1 <= Constants.MAX_CHECKPOINTS_PER_EPOCH; | ||
| require( | ||
| claimedNumCheckpointsInEpoch, | ||
| Errors.Rollup__TooManyCheckpointsInEpoch(Constants.MAX_CHECKPOINTS_PER_EPOCH, _end - _start) | ||
| ); | ||
|
|
||
| return (endEpoch, currentEpoch, provenBeforeSubmission); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -534,8 +554,8 @@ library EpochProofLib { | |
| * 2. Assembling the public inputs for the root rollup circuit | ||
| * 3. Verifying the validity proof against the assembled public inputs using the configured verifier | ||
| * | ||
| * @dev Assumes the caller has already verified the supplied checkpoint headers against storage, so assembly skips | ||
| * rehashing them. | ||
| * @dev Assumes the caller has completed the submit path's required header checks, so assembly does not rehash | ||
| * headers. | ||
| * | ||
| * @dev Errors Thrown: | ||
| * - Rollup__InvalidBlobProof: Batched blob proof verification failed | ||
|
|
||
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
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When would these two differ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This covers when proofs for epoch N land in epoch N + 1.
Assuming N = 1, epoch 1 covers checkpoints 1..32, epoch 2 covers checkpoints 33..64
We could receive proofs for epoch 1 during epoch 2's lifetime while epoch 2 is partial proven. Let's assume we've proven the first 3 checkpoints of epoch 2 when another epoch 1 proof lands:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But is there any scenario where
accountedPrefixLengthis greater thanprovenPrefixLength? Can't we just useRewardLib.getLongestProvenLength(endEpoch)?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Astra here (the AI assistant), posting this explanation at Santiago's request; this is my analysis, not Santiago writing.
@alexghr Your example makes sense: the global proven tip can include checkpoints from the next epoch, so
provenPrefixLengthcan exceed this epoch'saccountedPrefixLength. Following up on Santiago's question, though, can the reverse ever happen? Theminonly changes the result relative to usingaccountedPrefixLengthdirectly ifaccountedPrefixLength > provenPrefixLength.For a smaller example, suppose epoch A contains checkpoints 1–4 and epoch B contains 5–8. Each row below is a successive submission; the counters are their values before that submission:
In the last row, the current calculation gives:
The 6 includes checkpoints from B, whereas the value we need is simply how many checkpoints in A were already proven and accounted for.
From the production state transitions I reviewed, the invariant appears to be:
longestProvenLengthis extended only through successful proof submission.Therefore, whenever the epoch has an accounted prefix, the global proven tip must already cover that prefix. If the tip is before the epoch's start, its accounted length should be zero. I don't see a reachable state through normal completed submissions where accounting gets ahead of proving.
Could we consequently replace the tuple/conditional/
minblock with:That would also let
assertAcceptablereturn justendEpoch; its internal proven-tip acceptance check would remain unchanged.A shorter repeat proof is also worth considering: if A already has accounted length 4 and someone submits A: 1–2, starting verification at index 4 simply skips the loop, since both submitted headers were already verified. The existing header-count validation remains in place.
Is there a production path that breaks this invariant? If not, using the epoch-local accounted length directly seems equivalent and easier to follow.