-
Notifications
You must be signed in to change notification settings - Fork 599
feat: Handle epoch proofs on L1 #8704
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -447,6 +447,189 @@ contract Rollup is Leonidas, IRollup, ITestRollup { | |
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Submit a proof for an epoch in the pending chain | ||
| * | ||
| * @dev Will emit `L2ProofVerified` if the proof is valid | ||
| * | ||
| * @dev Will throw if: | ||
| * - The block number is past the pending chain | ||
| * - The last archive root of the header does not match the archive root of parent block | ||
| * - The archive root of the header does not match the archive root of the proposed block | ||
| * - The proof is invalid | ||
| * | ||
| * @dev We provide the `_archive` and `_blockHash` even if it could be read from storage itself because it allow for | ||
| * better error messages. Without passing it, we would just have a proof verification failure. | ||
| * | ||
| * @param _epochSize - The size of the epoch (to be promoted to a constant) | ||
| * @param _args - Array of public inputs to the proof (previousArchive, endArchive, previousBlockHash, endBlockHash, endTimestamp, outHash, proverId) | ||
| * @param _fees - Array of recipient-value pairs with fees to be distributed for the epoch | ||
| * @param _aggregationObject - The aggregation object for the proof | ||
| * @param _proof - The proof to verify | ||
| */ | ||
| function submitEpochRootProof( | ||
| uint256 _epochSize, | ||
| bytes32[7] calldata _args, | ||
| bytes32[64] calldata _fees, | ||
| bytes calldata _aggregationObject, | ||
| bytes calldata _proof | ||
| ) external override(IRollup) { | ||
| uint256 previousBlockNumber = tips.provenBlockNumber; | ||
| uint256 endBlockNumber = previousBlockNumber + _epochSize; | ||
|
|
||
| bytes32[] memory publicInputs = | ||
| getEpochProofPublicInputs(_epochSize, _args, _fees, _aggregationObject); | ||
|
|
||
| if (!verifier.verify(_proof, publicInputs)) { | ||
| revert Errors.Rollup__InvalidProof(); | ||
| } | ||
|
|
||
| tips.provenBlockNumber = endBlockNumber; | ||
|
|
||
| for (uint256 i = 0; i < 32; i++) { | ||
| address coinbase = address(uint160(uint256(publicInputs[9 + i * 2]))); | ||
| uint256 fees = uint256(publicInputs[10 + i * 2]); | ||
|
|
||
| if (coinbase != address(0) && fees > 0) { | ||
| // @note This will currently fail if there are insufficient funds in the bridge | ||
| // which WILL happen for the old version after an upgrade where the bridge follow. | ||
| // Consider allowing a failure. See #7938. | ||
| FEE_JUICE_PORTAL.distributeFees(coinbase, fees); | ||
| } | ||
| } | ||
|
|
||
| emit L2ProofVerified(endBlockNumber, _args[6]); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Returns the computed public inputs for the given epoch proof. | ||
| * | ||
| * @dev Useful for debugging and testing. Allows submitter to compare their | ||
| * own public inputs used for generating the proof vs the ones assembled | ||
| * by this contract when verifying it. | ||
| * | ||
| * @param _epochSize - The size of the epoch (to be promoted to a constant) | ||
| * @param _args - Array of public inputs to the proof (previousArchive, endArchive, previousBlockHash, endBlockHash, endTimestamp, outHash, proverId) | ||
| * @param _fees - Array of recipient-value pairs with fees to be distributed for the epoch | ||
| * @param _aggregationObject - The aggregation object for the proof | ||
| */ | ||
| function getEpochProofPublicInputs( | ||
| uint256 _epochSize, | ||
| bytes32[7] calldata _args, | ||
| bytes32[64] calldata _fees, | ||
| bytes calldata _aggregationObject | ||
| ) public view returns (bytes32[] memory) { | ||
| uint256 previousBlockNumber = tips.provenBlockNumber; | ||
| uint256 endBlockNumber = previousBlockNumber + _epochSize; | ||
|
|
||
| // Args are defined as an array because Solidity complains with "stack too deep" otherwise | ||
| // 0 bytes32 _previousArchive, | ||
| // 1 bytes32 _endArchive, | ||
| // 2 bytes32 _previousBlockHash, | ||
| // 3 bytes32 _endBlockHash, | ||
| // 4 bytes32 _endTimestamp, | ||
| // 5 bytes32 _outHash, | ||
| // 6 bytes32 _proverId, | ||
|
|
||
| // TODO(#7373): Public inputs are not fully verified | ||
|
|
||
| { | ||
| // We do it this way to provide better error messages than passing along the storage values | ||
| bytes32 expectedPreviousArchive = blocks[previousBlockNumber].archive; | ||
| if (expectedPreviousArchive != _args[0]) { | ||
| revert Errors.Rollup__InvalidPreviousArchive(expectedPreviousArchive, _args[0]); | ||
| } | ||
|
|
||
| bytes32 expectedEndArchive = blocks[endBlockNumber].archive; | ||
| if (expectedEndArchive != _args[1]) { | ||
| revert Errors.Rollup__InvalidArchive(expectedEndArchive, _args[1]); | ||
| } | ||
|
|
||
| bytes32 expectedPreviousBlockHash = blocks[previousBlockNumber].blockHash; | ||
| if (expectedPreviousBlockHash != _args[2]) { | ||
| revert Errors.Rollup__InvalidPreviousBlockHash(expectedPreviousBlockHash, _args[2]); | ||
| } | ||
|
|
||
| bytes32 expectedEndBlockHash = blocks[endBlockNumber].blockHash; | ||
| if (expectedEndBlockHash != _args[3]) { | ||
| revert Errors.Rollup__InvalidBlockHash(expectedEndBlockHash, _args[3]); | ||
| } | ||
| } | ||
|
|
||
| bytes32[] memory publicInputs = new bytes32[]( | ||
| Constants.ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH + Constants.AGGREGATION_OBJECT_LENGTH | ||
| ); | ||
|
|
||
| // Structure of the root rollup public inputs we need to reassemble: | ||
| // | ||
| // struct RootRollupPublicInputs { | ||
| // previous_archive: AppendOnlyTreeSnapshot, | ||
| // end_archive: AppendOnlyTreeSnapshot, | ||
| // previous_block_hash: Field, | ||
| // end_block_hash: Field, | ||
| // end_timestamp: u64, | ||
| // end_block_number: Field, | ||
| // out_hash: Field, | ||
| // fees: [FeeRecipient; 32], | ||
| // vk_tree_root: Field, | ||
| // prover_id: Field | ||
| // } | ||
|
|
||
| // previous_archive.root: the previous archive tree root | ||
| publicInputs[0] = _args[0]; | ||
|
|
||
| // previous_archive.next_available_leaf_index: the previous archive next available index | ||
| // normally this should be equal to the block number (since leaves are 0-indexed and blocks 1-indexed) | ||
| // but in yarn-project/merkle-tree/src/new_tree.ts we prefill the tree so that block N is in leaf N | ||
| publicInputs[1] = bytes32(previousBlockNumber + 1); | ||
|
|
||
| // end_archive.root: the new archive tree root | ||
| publicInputs[2] = _args[1]; | ||
|
|
||
| // end_archive.next_available_leaf_index: the new archive next available index | ||
| publicInputs[3] = bytes32(endBlockNumber + 1); | ||
|
|
||
| // previous_block_hash: the block hash just preceding this epoch | ||
| publicInputs[4] = _args[2]; | ||
|
|
||
| // end_block_hash: the last block hash in the epoch | ||
| publicInputs[5] = _args[3]; | ||
|
|
||
| // end_timestamp: the timestamp of the last block in the epoch | ||
| publicInputs[6] = _args[4]; | ||
|
Contributor
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. Why are we providing this as an input? Given the end block number, we can read the slotnumber for it and compute the timestamp. This might get slightly "odd" with the based fallback, but those should be proven individually anyway.
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. I didn't know timestamps were derived directly from slots, I thought the sequencer had freedom to choose the timestamp within a range as in Ethereum (or in Ethereum pre-merge at least, not sure if it changed in PoS). But yeah, we should be able to remove this.
Contributor
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. We are deriving them directly from the slots, there is a |
||
|
|
||
| // end_block_number: last block number in the epoch | ||
| publicInputs[7] = bytes32(endBlockNumber); | ||
|
|
||
| // out_hash: root of this epoch's l2 to l1 message tree | ||
| publicInputs[8] = _args[5]; | ||
|
Contributor
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 one is not clear to me, why are we providing this value? Also, it can be chosen freely, so if it just matching something in the circuit we don't need it is a public value?
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. I think it can be removed as well. |
||
|
|
||
| // fees[9-40]: array of recipient-value pairs | ||
| for (uint256 i = 0; i < 64; i++) { | ||
| publicInputs[9 + i] = _fees[i]; | ||
| } | ||
|
|
||
| // vk_tree_root | ||
| publicInputs[41] = vkTreeRoot; | ||
|
|
||
| // prover_id: id of current epoch's prover | ||
| publicInputs[42] = _args[6]; | ||
|
Contributor
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. Out of interest here, what is the prover id, an address? Can you point me at a definition somewhere 👀
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. Can be anything, even a string. We're just using it to identify the provers in the context of integrations. My guess is we'll eventually replace this with some identifier that is also used during coordination and escrows, possibly an L1 address. |
||
|
|
||
| // the block proof is recursive, which means it comes with an aggregation object | ||
| // this snippet copies it into the public inputs needed for verification | ||
| // it also guards against empty _aggregationObject used with mocked proofs | ||
| uint256 aggregationLength = _aggregationObject.length / 32; | ||
| for (uint256 i = 0; i < Constants.AGGREGATION_OBJECT_LENGTH && i < aggregationLength; i++) { | ||
| bytes32 part; | ||
| assembly { | ||
|
Contributor
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. I think you could do a larger
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. Mind if I push it for later, being just a gas optimization?
Contributor
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. Yep, is fine 👍 |
||
| part := calldataload(add(_aggregationObject.offset, mul(i, 32))) | ||
| } | ||
| publicInputs[i + 43] = part; | ||
| } | ||
|
|
||
| return publicInputs; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Check if msg.sender can propose at a given time | ||
| * | ||
|
|
||
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.
On these block hashes, I am still not fully getting why they touch L1. When a block is proposed they are stored, but not check in any way and here we use them to ensure that the proof was made with the same blockhash but seems like that would already be there from just the archives identifying the blocks. If we were computing it from the header on contract would make more sense.
Note, this is not really an issue specifically for this pr, but more general on the blockhash stored.
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.
Agree. I think we could remove them from the public inputs altogether: checking the archive root should be equivalent to checking the block hash. But I don't want to introduce changes to those circuits now.