diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 283aafc48284..e1664ed895e3 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -403,6 +403,166 @@ contract Rollup is Leonidas, IRollup, ITestRollup { emit L2ProofVerified(header.globalVariables.blockNumber, _proverId); } + // 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, + + /** + * @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 _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; + + // _args are defined like this 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, + + // Public inputs are not fully verified (TODO(#7373)) + + { + // 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__InvalidArchive(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__InvalidBlockHash(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 + ); + + // From noir-projects/noir-protocol-circuits/crates/rollup-lib/src/root/root_rollup_public_inputs.nr: 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]; + + // 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]; + + // 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]; + + // 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 { + part := calldataload(add(_aggregationObject.offset, mul(i, 32))) + } + publicInputs[i + 43] = part; + } + + 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 Check if msg.sender can propose at a given time * diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 1f02808bf960..4a15adafaa20 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -38,7 +38,16 @@ interface IRollup { bytes calldata _proof ) external; + function submitEpochRootProof( + uint256 _epochSize, + bytes32[7] calldata _args, + bytes32[64] calldata _fees, + bytes calldata _aggregationObject, + bytes calldata _proof + ) external; + function canProposeAtTime(uint256 _ts, bytes32 _archive) external view returns (uint256, uint256); + function validateHeader( bytes calldata _header, SignatureLib.Signature[] memory _signatures, diff --git a/l1-contracts/src/core/libraries/ConstantsGen.sol b/l1-contracts/src/core/libraries/ConstantsGen.sol index d2f8d7ada134..7f2180ddc18b 100644 --- a/l1-contracts/src/core/libraries/ConstantsGen.sol +++ b/l1-contracts/src/core/libraries/ConstantsGen.sol @@ -202,6 +202,8 @@ library Constants { uint256 internal constant CONSTANT_ROLLUP_DATA_LENGTH = 12; uint256 internal constant BASE_OR_MERGE_PUBLIC_INPUTS_LENGTH = 29; uint256 internal constant BLOCK_ROOT_OR_BLOCK_MERGE_PUBLIC_INPUTS_LENGTH = 91; + uint256 internal constant FEE_RECIPIENT_LENGTH = 2; + uint256 internal constant ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH = 75; uint256 internal constant GET_NOTES_ORACLE_RETURN_LENGTH = 674; uint256 internal constant NOTE_HASHES_NUM_BYTES_PER_BASE_ROLLUP = 2048; uint256 internal constant NULLIFIERS_NUM_BYTES_PER_BASE_ROLLUP = 2048; diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index db826807b5ec..c8eee01d2ac4 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -47,6 +47,7 @@ library Errors { error Rollup__InvalidArchive(bytes32 expected, bytes32 actual); // 0xb682a40e error Rollup__InvalidProposedArchive(bytes32 expected, bytes32 actual); // 0x32532e73 error Rollup__InvalidBlockNumber(uint256 expected, uint256 actual); // 0xe5edf847 + error Rollup__InvalidBlockHash(bytes32 expected, bytes32 actual); error Rollup__SlotValueTooLarge(uint256 slot); // 0x7234f4fe error Rollup__SlotAlreadyInChain(uint256 lastSlot, uint256 proposedSlot); // 0x83510bd0 error Rollup__InvalidEpoch(uint256 expected, uint256 actual); // 0x3c6d65e6 diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/abis/block_root_or_block_merge_public_inputs.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/abis/block_root_or_block_merge_public_inputs.nr index 4c5fb902225f..fea07d0066c7 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/abis/block_root_or_block_merge_public_inputs.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/abis/block_root_or_block_merge_public_inputs.nr @@ -1,7 +1,7 @@ use dep::types::{ abis::{append_only_tree_snapshot::AppendOnlyTreeSnapshot, global_variables::GlobalVariables}, - constants::BLOCK_ROOT_OR_BLOCK_MERGE_PUBLIC_INPUTS_LENGTH, traits::{Empty, Serialize, Deserialize}, - utils::reader::Reader, address::EthAddress + constants::{BLOCK_ROOT_OR_BLOCK_MERGE_PUBLIC_INPUTS_LENGTH, FEE_RECIPIENT_LENGTH}, + traits::{Empty, Serialize, Deserialize}, utils::reader::Reader, address::EthAddress }; struct FeeRecipient { @@ -16,14 +16,14 @@ impl Empty for FeeRecipient { } } -impl Serialize<2> for FeeRecipient { - fn serialize(self) -> [Field; 2] { +impl Serialize for FeeRecipient { + fn serialize(self) -> [Field; FEE_RECIPIENT_LENGTH] { [self.recipient.to_field(), self.value] } } -impl Deserialize<2> for FeeRecipient { - fn deserialize(values: [Field; 2]) -> Self { +impl Deserialize for FeeRecipient { + fn deserialize(values: [Field; FEE_RECIPIENT_LENGTH]) -> Self { Self { recipient: EthAddress::from_field(values[0]), value: values[1] } } } diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/block_root_rollup_inputs.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/block_root_rollup_inputs.nr index f32c144b9cc3..c04e3d91260c 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/block_root_rollup_inputs.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/block_root_rollup_inputs.nr @@ -16,6 +16,7 @@ use types::{ merkle_tree::{append_only_tree, calculate_empty_tree_root}, state_reference::StateReference, traits::Empty }; +use types::debug_log::debug_log_format; global ALLOWED_PREVIOUS_CIRCUITS = [ BASE_ROLLUP_INDEX, @@ -35,6 +36,7 @@ struct BlockRootRollupInputs { start_l1_to_l2_message_tree_snapshot: AppendOnlyTreeSnapshot, // inputs required to add the block hash + // TODO: Remove in favor of left.constants.last_archive start_archive_snapshot: AppendOnlyTreeSnapshot, new_archive_sibling_path: [Field; ARCHIVE_HEIGHT], // Added previous_block_hash to be passed through to the final root, where it will be either: @@ -93,6 +95,24 @@ impl BlockRootRollupInputs { let total_fees = components::accumulate_fees(left, right); + // unsafe { + // debug_log_format("Assembling header in block root rollup", []); + // debug_log_format( + // "header.last_archive={}", + // left.constants.last_archive.serialize() + // ); + // debug_log_format( + // "header.content_commitment={}", + // content_commitment.serialize() + // ); + // debug_log_format("header.state={}", state.serialize()); + // debug_log_format( + // "header.global_variables={}", + // left.constants.global_variables.serialize() + // ); + // debug_log_format("header.total_fees={0}", [total_fees]); + // } + let header = Header { last_archive: left.constants.last_archive, content_commitment, diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/root/root_rollup_public_inputs.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/root/root_rollup_public_inputs.nr index 15b239b8714b..b8f2d4622926 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/root/root_rollup_public_inputs.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/root/root_rollup_public_inputs.nr @@ -1,6 +1,6 @@ use dep::types::abis::append_only_tree_snapshot::AppendOnlyTreeSnapshot; use crate::abis::block_root_or_block_merge_public_inputs::FeeRecipient; -// TODO(#7346): Currently unused! Will be used when batch rollup circuits are integrated. + struct RootRollupPublicInputs { // Snapshot of archive tree before/after this rollup has been processed previous_archive: AppendOnlyTreeSnapshot, diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index bc68ac6eb316..7b8cec518876 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -268,6 +268,9 @@ global BASE_OR_MERGE_PUBLIC_INPUTS_LENGTH: u32 = CONSTANT_ROLLUP_DATA_LENGTH + P // + 64 for 32 * FeeRecipient { recipient, value }, + 4 for previous_block_hash, end_block_hash, out_hash, vk_tree_root + 1 temporarily for prover_id global BLOCK_ROOT_OR_BLOCK_MERGE_PUBLIC_INPUTS_LENGTH: u32 = 2 * APPEND_ONLY_TREE_SNAPSHOT_LENGTH + 2 * GLOBAL_VARIABLES_LENGTH + 69; +global FEE_RECIPIENT_LENGTH: u32 = 2; +global ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH: u32 = 2 * APPEND_ONLY_TREE_SNAPSHOT_LENGTH + 7 + 32 * FEE_RECIPIENT_LENGTH; + global GET_NOTES_ORACLE_RETURN_LENGTH: u32 = 674; global NOTE_HASHES_NUM_BYTES_PER_BASE_ROLLUP: u32 = 32 * MAX_NOTE_HASHES_PER_TX; global NULLIFIERS_NUM_BYTES_PER_BASE_ROLLUP: u32 = 32 * MAX_NULLIFIERS_PER_TX; diff --git a/yarn-project/circuits.js/src/constants.gen.ts b/yarn-project/circuits.js/src/constants.gen.ts index 2dd38b812f2b..f674af0fff1a 100644 --- a/yarn-project/circuits.js/src/constants.gen.ts +++ b/yarn-project/circuits.js/src/constants.gen.ts @@ -184,6 +184,8 @@ export const KERNEL_CIRCUIT_PUBLIC_INPUTS_LENGTH = 663; export const CONSTANT_ROLLUP_DATA_LENGTH = 12; export const BASE_OR_MERGE_PUBLIC_INPUTS_LENGTH = 29; export const BLOCK_ROOT_OR_BLOCK_MERGE_PUBLIC_INPUTS_LENGTH = 91; +export const FEE_RECIPIENT_LENGTH = 2; +export const ROOT_ROLLUP_PUBLIC_INPUTS_LENGTH = 75; export const GET_NOTES_ORACLE_RETURN_LENGTH = 674; export const NOTE_HASHES_NUM_BYTES_PER_BASE_ROLLUP = 2048; export const NULLIFIERS_NUM_BYTES_PER_BASE_ROLLUP = 2048;