From ea15da4dfd839278ae7b2182874c6bff82f2264b Mon Sep 17 00:00:00 2001 From: kevaundray Date: Fri, 3 Nov 2023 02:27:57 +0000 Subject: [PATCH 01/11] add `assert_check_membership` this is check_membership in the cpp code --- .../noir-protocol-circuits/src/crates/types/src/hash.nr | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/yarn-project/noir-protocol-circuits/src/crates/types/src/hash.nr b/yarn-project/noir-protocol-circuits/src/crates/types/src/hash.nr index 614d93eccae..d023a107954 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/types/src/hash.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/types/src/hash.nr @@ -15,6 +15,13 @@ use dep::aztec::{ hash::sha256_to_field, }; +// Checks that `value` is a member of a merkle tree with root `root` at position `index` +// The witness being the `sibling_path` +pub fn assert_check_membership(value : Field, index : Field, sibling_path : [Field; N], root : Field) { + let calculated_root = root_from_sibling_path(value, index, sibling_path); + assert(calculated_root == root, "membership check failed"); +} + // Calculate the Merkle tree root from the sibling path and leaf. // // The leaf is hashed with its sibling, and then the result is hashed @@ -25,7 +32,7 @@ use dep::aztec::{ // TODO: I'd generally like to avoid u256 for algorithms like // this because it means we never even need to consider cases where // the index is greater than p. -fn root_from_sibling_path(leaf : Field, leaf_index : Field, sibling_path : [Field; N]) -> Field { +pub fn root_from_sibling_path(leaf : Field, leaf_index : Field, sibling_path : [Field; N]) -> Field { let mut node = leaf; let indices = leaf_index.to_le_bits(N); From 2306f9a43dbf8f39bc126fa33dd6cdb5c6a182e1 Mon Sep 17 00:00:00 2001 From: kevaundray Date: Fri, 3 Nov 2023 02:30:22 +0000 Subject: [PATCH 02/11] add hash module for computing hashes that are only relevant to the rollup circuits --- .../src/crates/rollup-lib/src/hash.nr | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/hash.nr diff --git a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/hash.nr b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/hash.nr new file mode 100644 index 00000000000..2cf61bc19e8 --- /dev/null +++ b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/hash.nr @@ -0,0 +1,15 @@ +use crate::abis::global_variables::GlobalVariables; +use dep::aztec::constants_gen; + +pub fn compute_block_hash_with_globals( + globals : GlobalVariables, + note_hash_tree_root : Field, + nullifier_tree_root : Field, + contract_tree_root : Field, + l1_to_l2_data_tree_root : Field, + public_data_tree_root : Field) -> Field { + + let inputs = [globals.hash(), note_hash_tree_root, nullifier_tree_root, contract_tree_root, l1_to_l2_data_tree_root, public_data_tree_root]; + + dep::std::hash::pedersen_hash_with_separator(inputs, constants_gen::GENERATOR_INDEX__BLOCK_HASH) +} \ No newline at end of file From 2e9cde4e4261653d7c49dd2c198a2c6a55c3c41c Mon Sep 17 00:00:00 2001 From: kevaundray Date: Fri, 3 Nov 2023 02:30:31 +0000 Subject: [PATCH 03/11] add analogous code to "in-memory" merkle tree hashing --- .../src/crates/rollup-lib/src/lib.nr | 4 + .../src/crates/rollup-lib/src/merkle_tree.nr | 164 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr diff --git a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/lib.nr b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/lib.nr index e9202bb147e..2e9a4871da0 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/lib.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/lib.nr @@ -10,3 +10,7 @@ mod merge; mod root; mod components; + +mod hash; + +mod merkle_tree; \ No newline at end of file diff --git a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr new file mode 100644 index 00000000000..a253b7fd802 --- /dev/null +++ b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr @@ -0,0 +1,164 @@ +// This is the naive way to do the merkle tree hashing given we know the height. +// Its janky but has the least complexity given that for loop bounds need to be constant. +// +// TODO: Check that in all cases the compiler is able to see that num_of_leaves +// is a constant, else this will be pretty expensive. +pub fn calculate_subtree(leaves : [Field; N]) -> Field { + let num_of_leaves = leaves.len(); + if num_of_leaves == 128 { + calculate_subtree_128(leaves.as_slice()) + } else if num_of_leaves == 64 { + calculate_subtree_64(leaves.as_slice()) + } else if num_of_leaves == 32 { + calculate_subtree_32(leaves.as_slice()) + } else if num_of_leaves == 16 { + calculate_subtree_16(leaves.as_slice()) + } else if num_of_leaves == 8 { + calculate_subtree_8(leaves.as_slice()) + } else if num_of_leaves == 4 { + calculate_subtree_4(leaves.as_slice()) + } else if num_of_leaves == 2 { + calculate_subtree_2(leaves.as_slice()) + } else { + assert(false, "number of leaves should be 2, 4, 8, or 16"); + 0 + } +} + +fn calculate_subtree_128(leaves : [Field]) -> Field { + assert(leaves.len() == 128, "number of leaves should be 128"); + + let mut layer = [0; 64]; + for i in 0..layer.len() { + let combined = [leaves[2 * i], leaves[2 * i + 1]]; + layer[i] = dep::std::hash::pedersen_hash(combined); + } + + calculate_subtree_64(layer.as_slice()) +} +fn calculate_subtree_64(leaves : [Field]) -> Field { + assert(leaves.len() == 64, "number of leaves should be 64"); + + let mut layer = [0; 32]; + for i in 0..layer.len() { + let combined = [leaves[2 * i], leaves[2 * i + 1]]; + layer[i] = dep::std::hash::pedersen_hash(combined); + } + + calculate_subtree_32(layer.as_slice()) +} +fn calculate_subtree_32(leaves : [Field]) -> Field { + assert(leaves.len() == 32, "number of leaves should be 32"); + + let mut layer = [0; 16]; + for i in 0..layer.len() { + let combined = [leaves[2 * i], leaves[2 * i + 1]]; + layer[i] = dep::std::hash::pedersen_hash(combined); + } + + calculate_subtree_16(layer.as_slice()) +} +fn calculate_subtree_16(leaves : [Field]) -> Field { + assert(leaves.len() == 16, "number of leaves should be 16"); + + let mut layer = [0; 8]; + for i in 0..layer.len() { + let combined = [leaves[2 * i], leaves[2 * i + 1]]; + layer[i] = dep::std::hash::pedersen_hash(combined); + } + + calculate_subtree_8(layer.as_slice()) +} +fn calculate_subtree_8(leaves : [Field]) -> Field { + assert(leaves.len() == 8, "number of leaves should be 8"); + + let mut layer = [0; 4]; + for i in 0..layer.len() { + let combined = [leaves[2 * i], leaves[2 * i + 1]]; + layer[i] = dep::std::hash::pedersen_hash(combined); + } + + calculate_subtree_4(layer) +} +fn calculate_subtree_4(leaves : [Field]) -> Field { + assert(leaves.len() == 4, "number of leaves should be 4"); + + let mut layer = [0; 2]; + for i in 0..layer.len() { + let combined = [leaves[2 * i], leaves[2 * i + 1]]; + layer[i] = dep::std::hash::pedersen_hash(combined); + } + + calculate_subtree_2(layer.as_slice()) +} +fn calculate_subtree_2(leaves : [Field]) -> Field { + dep::std::hash::pedersen_hash(leaves) +} + +// These values are precomputed and we run tests to ensure that they +// are correct. The values themselves were computed from the cpp code. +// +// Would be good if we could use width since the compute_subtree +// algorithm uses depth. +fn calculate_empty_tree_root(depth : Field) -> Field { + if depth == 1 { + 0x27b1d0839a5b23baf12a8d195b18ac288fcf401afb2f70b8a4b529ede5fa9fed + } else if depth == 2 { + 0x21dbfd1d029bf447152fcf89e355c334610d1632436ba170f738107266a71550 + } else if depth == 3{ + 0x0bcd1f91cf7bdd471d0a30c58c4706f3fdab3807a954b8f5b5e3bfec87d001bb + } else if depth == 4 { + 0x06e62084ee7b602fe9abc15632dda3269f56fb0c6e12519a2eb2ec897091919d + } else if depth == 5 { + 0x03c9e2e67178ac638746f068907e6677b4cc7a9592ef234ab6ab518f17efffa0 + } else if depth == 6 { + 0x15d28cad4c0736decea8997cb324cf0a0e0602f4d74472cd977bce2c8dd9923f + } else if depth == 7 { + 0x268ed1e1c94c3a45a14db4108bc306613a1c23fab68e0466a002dfb0a3f8d2ab + } else if depth == 8 { + 0x0cd8d5695bc2dde99dd531671f76f1482f14ddba8eeca7cb9686d4a62359c257 + } else if depth == 9 { + 0x047fbb7eb974155702149e58ea6ad91f4c6e953e693db35e953e250d8ceac9a9 + } else if depth == 10 { + 0x00c5ae2526e665e2c7c698c11a06098b7159f720606d50e7660deb55758b0b02 + } else { + assert(false, "depth should be between 1 and 10"); + 0 + } +} + +#[test] +fn test_merkle_root_interop_test() { + // This is a test to ensure that we match the cpp implementation. + // You can grep for `TEST_F(root_rollup_tests, noir_interop_test)` + // to find the test that matches this. + let root = calculate_subtree([1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4]); + assert(0x17e8bb70a11d0c946345950879484d2f4f9fef397ff6adbfdec3baab2d41faab == root); + + let empty_root = calculate_subtree([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); + assert(0x06e62084ee7b602fe9abc15632dda3269f56fb0c6e12519a2eb2ec897091919d == empty_root); +} + +#[test] +fn test_empty_subroot() { + let expected_empty_root_2 = calculate_subtree([0,0]); + assert(calculate_empty_tree_root(1) == expected_empty_root_2); + + let expected_empty_root_4 = calculate_subtree([0,0,0,0]); + assert(calculate_empty_tree_root(2) == expected_empty_root_4); + + let expected_empty_root_8 = calculate_subtree([0,0,0,0,0,0,0,0]); + assert(calculate_empty_tree_root(3) == expected_empty_root_8); + + let expected_empty_root_16 = calculate_subtree([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); + assert(calculate_empty_tree_root(4) == expected_empty_root_16); + + let expected_empty_root_32 = calculate_subtree([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); + assert(calculate_empty_tree_root(5) == expected_empty_root_32); + + let expected_empty_root_64 = calculate_subtree([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); + assert(calculate_empty_tree_root(6) == expected_empty_root_64); + + let expected_empty_root_128 = calculate_subtree([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); + assert(calculate_empty_tree_root(7) == expected_empty_root_128); +} \ No newline at end of file From 5ee3055b59c8858e3a9fe344eb7aca37dce95fbd Mon Sep 17 00:00:00 2001 From: kevaundray Date: Fri, 3 Nov 2023 02:31:10 +0000 Subject: [PATCH 04/11] add code to `insert_subtree_to_snapshot_tree` --- .../src/crates/rollup-lib/src/components.nr | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/components.nr b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/components.nr index fd86510fa41..4eb6d8bffd8 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/components.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/components.nr @@ -1,9 +1,10 @@ use crate::abis::base_or_merge_rollup_public_inputs::BaseOrMergeRollupPublicInputs; use dep::types::mocked::AggregationObject; -use dep::types::hash::accumulate_sha256; +use dep::types::hash::{accumulate_sha256, assert_check_membership, root_from_sibling_path}; use dep::types::utils::uint128::U128; use dep::aztec::constants_gen::NUM_FIELDS_PER_SHA256; use crate::abis::previous_rollup_data::PreviousRollupData; +use crate::abis::append_only_tree_snapshot::AppendOnlyTreeSnapshot; /** * Create an aggregation object for the proofs that are provided @@ -30,7 +31,7 @@ pub fn assert_both_input_proofs_of_same_rollup_type(left : BaseOrMergeRollupPubl * Asserts that the rollup subtree heights are the same and returns the height * Returns the height of the rollup subtrees */ -pub fn assert_both_input_proofs_of_same_height_and_return(left : BaseOrMergeRollupPublicInputs, right : BaseOrMergeRollupPublicInputs) -> Field{ +pub fn assert_both_input_proofs_of_same_height_and_return(left : BaseOrMergeRollupPublicInputs, right : BaseOrMergeRollupPublicInputs) -> Field { assert(left.rollup_subtree_height == right.rollup_subtree_height, "input proofs are of different rollup heights"); left.rollup_subtree_height } @@ -67,3 +68,26 @@ pub fn compute_calldata_hash(previous_rollup_data : [PreviousRollupData ; 2]) -> U128::from_field(previous_rollup_data[1].base_or_merge_rollup_public_inputs.calldata_hash[1]) ]) } + +pub fn insert_subtree_to_snapshot_tree( + snapshot : AppendOnlyTreeSnapshot, + siblingPath : [Field; N], + emptySubtreeRoot : Field, + subtreeRootToInsert : Field, + subtreeDepth : u8, +) -> AppendOnlyTreeSnapshot { + // TODO(Lasse): Sanity check len of siblingPath > height of subtree + // TODO(Lasse): Ensure height of subtree is correct (eg 3 for commitments, 1 for contracts) + let leafIndexAtDepth = snapshot.next_available_leaf_index >> (subtreeDepth as u32); + + // Check that the current root is correct and that there is an empty subtree at the insertion location + assert_check_membership(emptySubtreeRoot, leafIndexAtDepth as Field, siblingPath, snapshot.root); + + // if index of leaf is x, index of its parent is x/2 or x >> 1. We need to find the parent `subtreeDepth` levels up. + let new_root = root_from_sibling_path(subtreeRootToInsert, leafIndexAtDepth as Field, siblingPath); + + // 2^subtreeDepth is the number of leaves added. 2^x = 1 << x + let new_next_available_leaf_index = (snapshot.next_available_leaf_index as u64) + (1 << (subtreeDepth as u64)); + + AppendOnlyTreeSnapshot{root: new_root, next_available_leaf_index: new_next_available_leaf_index as u32} +} \ No newline at end of file From c56ac3ccb908ff83b07e03bd043b00597be3b9ff Mon Sep 17 00:00:00 2001 From: kevaundray Date: Fri, 3 Nov 2023 02:31:36 +0000 Subject: [PATCH 05/11] fill in `root_rollup_circuit` --- .../src/crates/rollup-lib/src/root.nr | 131 +++++++++++++++++- 1 file changed, 128 insertions(+), 3 deletions(-) diff --git a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/root.nr b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/root.nr index a3520900c5a..ffd15c0b08f 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/root.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/root.nr @@ -2,11 +2,136 @@ mod root_rollup_inputs; use root_rollup_inputs::RootRollupInputs; mod root_rollup_public_inputs; use root_rollup_public_inputs::RootRollupPublicInputs; - +use crate::abis::append_only_tree_snapshot::AppendOnlyTreeSnapshot; +use dep::types::utils::uint256::U256; +use dep::aztec::constants_gen::{NUM_FIELDS_PER_SHA256,NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP,L1_TO_L2_MSG_SUBTREE_HEIGHT}; +use crate::{components, hash::compute_block_hash_with_globals}; +use crate::merkle_tree::{calculate_subtree, calculate_empty_tree_root}; impl RootRollupInputs { pub fn root_rollup_circuit(self) -> RootRollupPublicInputs { - let zeroed = dep::std::unsafe::zeroed(); - zeroed + + let left = self.previous_rollup_data[0].base_or_merge_rollup_public_inputs; + let right = self.previous_rollup_data[1].base_or_merge_rollup_public_inputs; + + let aggregation_object = components::aggregate_proofs(left, right); + components::assert_both_input_proofs_of_same_rollup_type(left, right); + let _ = components::assert_both_input_proofs_of_same_height_and_return(left, right); + components::assert_equal_constants(left, right); + components::assert_prev_rollups_follow_on_from_each_other(left, right); + + // Check correct l1 to l2 tree given + // Compute subtree inserting l1 to l2 messages + let l1_to_l2_subtree_root = calculate_subtree(self.new_l1_to_l2_messages); + + // Insert subtree into the l1 to l2 data tree + let empty_l1_to_l2_subtree_root = calculate_empty_tree_root(L1_TO_L2_MSG_SUBTREE_HEIGHT); + let new_l1_to_l2_messages_tree_snapshot = components::insert_subtree_to_snapshot_tree( + self.start_l1_to_l2_messages_tree_snapshot, + self.new_l1_to_l2_messages_tree_root_sibling_path, + empty_l1_to_l2_subtree_root, + l1_to_l2_subtree_root, + // TODO(Kev): For now we can add a test that this fits inside of + // a u8. + L1_TO_L2_MSG_SUBTREE_HEIGHT as u8 + ); + + // Build the block hash for this iteration from the tree roots and global variables + // Then insert the block into the historic blocks tree + let block_hash = compute_block_hash_with_globals(left.constants.global_variables, + right.end_note_hash_tree_snapshot.root, + right.end_nullifier_tree_snapshot.root, + right.end_contract_tree_snapshot.root, + new_l1_to_l2_messages_tree_snapshot.root, + right.end_public_data_tree_root); + + // Update the historic blocks tree + let end_historic_blocks_tree_snapshot = components::insert_subtree_to_snapshot_tree( + self.start_historic_blocks_tree_snapshot, + self.new_historic_blocks_tree_sibling_path, + 0, + block_hash, + 0 + ); + + let zeroed_out_snapshot = AppendOnlyTreeSnapshot { + root : 0, + next_available_leaf_index : 0 + }; + + RootRollupPublicInputs{ + end_aggregation_object : aggregation_object, + global_variables : left.constants.global_variables, + start_note_hash_tree_snapshot : left.start_note_hash_tree_snapshot, + end_note_hash_tree_snapshot : right.end_note_hash_tree_snapshot, + start_nullifier_tree_snapshot : left.start_nullifier_tree_snapshot, + end_nullifier_tree_snapshot : right.end_nullifier_tree_snapshot, + start_contract_tree_snapshot : left.start_contract_tree_snapshot, + end_contract_tree_snapshot : right.end_contract_tree_snapshot, + start_public_data_tree_root : left.start_public_data_tree_root, + end_public_data_tree_root : right.end_public_data_tree_root, + start_l1_to_l2_messages_tree_snapshot : self.start_l1_to_l2_messages_tree_snapshot, + end_l1_to_l2_messages_tree_snapshot : new_l1_to_l2_messages_tree_snapshot, + start_historic_blocks_tree_snapshot : self.start_historic_blocks_tree_snapshot, + end_historic_blocks_tree_snapshot : end_historic_blocks_tree_snapshot, + calldata_hash : components::compute_calldata_hash(self.previous_rollup_data), + l1_to_l2_messages_hash : compute_messages_hash(self.new_l1_to_l2_messages), + + // The cpp code was just not initializing these, so they would be zeroed out + // TODO(Lasse/Jean): add explanation for this. + end_tree_of_historic_contract_tree_roots_snapshot : zeroed_out_snapshot, + end_tree_of_historic_l1_to_l2_messages_tree_roots_snapshot : zeroed_out_snapshot, + end_tree_of_historic_note_hash_tree_roots_snapshot : zeroed_out_snapshot, + start_tree_of_historic_contract_tree_roots_snapshot : zeroed_out_snapshot, + start_tree_of_historic_l1_to_l2_messages_tree_roots_snapshot : zeroed_out_snapshot, + start_tree_of_historic_note_hash_tree_roots_snapshot : zeroed_out_snapshot, + } + } +} + +// See `test_message_input_flattened_length` on keeping this in sync, +// why its here and how this constant was computed. +global NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP_NUM_BYTES = 512; + +// Computes the messages hash from the leaves array +// +// Returns the hash split into two field elements +fn compute_messages_hash(leaves : [Field; NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP]) -> [Field;NUM_FIELDS_PER_SHA256] { + + // Slice variation + // let mut hash_input_flattened = []; + // for leaf in leaves { + // let input_as_bytes = leaf.to_be_bytes(32); + // for i in 0..32 { + // // TODO(Kev): should check the complexity of repeatedly pushing + // hash_input_flattened.push(input_as_bytes[i]); + // } + // } + + // Convert each field element into a byte array and append the bytes to `hash_input_flattened` + let mut hash_input_flattened = [0; NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP_NUM_BYTES]; + for offset in 0..NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP { + let input_as_bytes = leaves[offset].to_be_bytes(32); + for byte_index in 0..32 { + hash_input_flattened[offset * 32 + byte_index] = input_as_bytes[byte_index]; + } } + + // Hash bytes and convert to 2 128 bit limbs + let sha_digest = dep::std::hash::sha256(hash_input_flattened); + // TODO(Kev): The CPP implementation is returning [high, low] + // and so is `to_u128_limbs`, so this matches. + // We should say why we are doing this vs [low, high] + U256::from_bytes32(sha_digest).to_u128_limbs() +} + +#[test] +fn test_message_input_flattened_length() { + // This is here so that the global doesn't become outdated. + // + // The short term solution to remove this is to use slices, though + // those are a bit experimental right now, so TODO I'll add a test that the + // slice version of compute_messages_hash is the same as the array version. + // which uses the NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP_NUM_BYTES global. + assert(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP * 32 == NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP_NUM_BYTES); } \ No newline at end of file From 009944516103ba932be9719577ecdb7a7768105e Mon Sep 17 00:00:00 2001 From: kevaundray Date: Fri, 3 Nov 2023 02:32:07 +0000 Subject: [PATCH 06/11] commit cpp test vectors for computing the merkle tree --- .../src/aztec3/circuits/rollup/root/.test.cpp | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/circuits/cpp/src/aztec3/circuits/rollup/root/.test.cpp b/circuits/cpp/src/aztec3/circuits/rollup/root/.test.cpp index 7d8e2242cbd..b9c05fe4bca 100644 --- a/circuits/cpp/src/aztec3/circuits/rollup/root/.test.cpp +++ b/circuits/cpp/src/aztec3/circuits/rollup/root/.test.cpp @@ -307,4 +307,33 @@ TEST_F(root_rollup_tests, native_root_missing_nullifier_logic) // run_cbind(rootRollupInputs, outputs, true); } +TEST_F(root_rollup_tests, noir_interop_test) +{ + // This is an annoying hack to convert the field into a hex string + // We should add a to_hex and from_hex method to field class + auto to_hex = [](const NT::fr& value) -> std::string { + std::stringstream field_as_hex_stream; + field_as_hex_stream << value; + return field_as_hex_stream.str(); + }; + + MemoryStore merkle_tree_store; + MerkleTree merkle_tree(merkle_tree_store, L1_TO_L2_MSG_SUBTREE_HEIGHT); + + std::array leaves = { 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4 }; + for (size_t i = 0; i < NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP; i++) { + merkle_tree.update_element(i, leaves[i]); + } + auto root = merkle_tree.root(); + auto expected = "0x17e8bb70a11d0c946345950879484d2f4f9fef397ff6adbfdec3baab2d41faab"; + ASSERT_EQ(to_hex(root), expected); + + // Empty subtree is the same as zeroes + MemoryStore empty_tree_store; + MerkleTree const empty_tree = MerkleTree(empty_tree_store, L1_TO_L2_MSG_SUBTREE_HEIGHT); + auto empty_root = empty_tree.root(); + auto expected_empty_root = "0x06e62084ee7b602fe9abc15632dda3269f56fb0c6e12519a2eb2ec897091919d"; + ASSERT_EQ(to_hex(empty_root), expected_empty_root); +} + } // namespace aztec3::circuits::rollup::root::native_root_rollup_circuit \ No newline at end of file From de018026258cdb7b741a4a0b2432c3a0f7fc6209 Mon Sep 17 00:00:00 2001 From: kevaundray Date: Fri, 3 Nov 2023 02:38:56 +0000 Subject: [PATCH 07/11] Update yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr --- .../src/crates/rollup-lib/src/merkle_tree.nr | 2 -- 1 file changed, 2 deletions(-) diff --git a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr index a253b7fd802..81429ca5740 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr @@ -1,8 +1,6 @@ // This is the naive way to do the merkle tree hashing given we know the height. // Its janky but has the least complexity given that for loop bounds need to be constant. // -// TODO: Check that in all cases the compiler is able to see that num_of_leaves -// is a constant, else this will be pretty expensive. pub fn calculate_subtree(leaves : [Field; N]) -> Field { let num_of_leaves = leaves.len(); if num_of_leaves == 128 { From ed5149edd1e284f4d8337c5ff56b2c83df217187 Mon Sep 17 00:00:00 2001 From: kevaundray Date: Fri, 3 Nov 2023 02:39:51 +0000 Subject: [PATCH 08/11] Update yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr --- .../src/crates/rollup-lib/src/merkle_tree.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr index 81429ca5740..0383fe731b5 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr @@ -18,7 +18,7 @@ pub fn calculate_subtree(leaves : [Field; N]) -> Field { } else if num_of_leaves == 2 { calculate_subtree_2(leaves.as_slice()) } else { - assert(false, "number of leaves should be 2, 4, 8, or 16"); + assert(false, "merkle tree width is not supported"); 0 } } From bf76904ffe8f2a48a52929f7c97a3c97f1c22c5d Mon Sep 17 00:00:00 2001 From: kevaundray Date: Fri, 3 Nov 2023 02:40:22 +0000 Subject: [PATCH 09/11] Update yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr --- .../src/crates/rollup-lib/src/merkle_tree.nr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr index 0383fe731b5..647844a8888 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr @@ -98,7 +98,7 @@ fn calculate_subtree_2(leaves : [Field]) -> Field { // // Would be good if we could use width since the compute_subtree // algorithm uses depth. -fn calculate_empty_tree_root(depth : Field) -> Field { +pub fn calculate_empty_tree_root(depth : Field) -> Field { if depth == 1 { 0x27b1d0839a5b23baf12a8d195b18ac288fcf401afb2f70b8a4b529ede5fa9fed } else if depth == 2 { From f9c853db090a9be25252d1c8b745d4d39a7a597a Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 3 Nov 2023 10:21:59 +0000 Subject: [PATCH 10/11] feat: add generic merkle tree --- .../src/crates/rollup-lib/src/merkle_tree.nr | 117 +++++------------- 1 file changed, 31 insertions(+), 86 deletions(-) diff --git a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr index 647844a8888..c603684d663 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr @@ -1,96 +1,40 @@ -// This is the naive way to do the merkle tree hashing given we know the height. -// Its janky but has the least complexity given that for loop bounds need to be constant. -// -pub fn calculate_subtree(leaves : [Field; N]) -> Field { - let num_of_leaves = leaves.len(); - if num_of_leaves == 128 { - calculate_subtree_128(leaves.as_slice()) - } else if num_of_leaves == 64 { - calculate_subtree_64(leaves.as_slice()) - } else if num_of_leaves == 32 { - calculate_subtree_32(leaves.as_slice()) - } else if num_of_leaves == 16 { - calculate_subtree_16(leaves.as_slice()) - } else if num_of_leaves == 8 { - calculate_subtree_8(leaves.as_slice()) - } else if num_of_leaves == 4 { - calculate_subtree_4(leaves.as_slice()) - } else if num_of_leaves == 2 { - calculate_subtree_2(leaves.as_slice()) - } else { - assert(false, "merkle tree width is not supported"); - 0 - } -} - -fn calculate_subtree_128(leaves : [Field]) -> Field { - assert(leaves.len() == 128, "number of leaves should be 128"); - - let mut layer = [0; 64]; - for i in 0..layer.len() { - let combined = [leaves[2 * i], leaves[2 * i + 1]]; - layer[i] = dep::std::hash::pedersen_hash(combined); - } - - calculate_subtree_64(layer.as_slice()) -} -fn calculate_subtree_64(leaves : [Field]) -> Field { - assert(leaves.len() == 64, "number of leaves should be 64"); - - let mut layer = [0; 32]; - for i in 0..layer.len() { - let combined = [leaves[2 * i], leaves[2 * i + 1]]; - layer[i] = dep::std::hash::pedersen_hash(combined); - } - - calculate_subtree_32(layer.as_slice()) +struct MerkleTree { + leaves: [Field; N], + nodes: [Field; N], } -fn calculate_subtree_32(leaves : [Field]) -> Field { - assert(leaves.len() == 32, "number of leaves should be 32"); - let mut layer = [0; 16]; - for i in 0..layer.len() { - let combined = [leaves[2 * i], leaves[2 * i + 1]]; - layer[i] = dep::std::hash::pedersen_hash(combined); +impl MerkleTree { + fn new(leaves: [Field; N]) -> Self { + let mut nodes = [0; N]; + + // We need one less node than leaves, but we cannot have computed array lengths + let total_nodes = N - 1; + let half_size = N/2; + + // hash base layer + for i in 0..half_size { + dep::std::println(i); + nodes[i] = dep::std::hash::pedersen_hash([leaves[2*i], leaves[2*i+1]]); + } + + // hash the other layers + for i in 0..(total_nodes - half_size) { + nodes[half_size+i] = dep::std::hash::pedersen_hash([nodes[2*i], nodes[2*i+1]]); + } + + MerkleTree { + leaves, + nodes, + } } - calculate_subtree_16(layer.as_slice()) -} -fn calculate_subtree_16(leaves : [Field]) -> Field { - assert(leaves.len() == 16, "number of leaves should be 16"); - - let mut layer = [0; 8]; - for i in 0..layer.len() { - let combined = [leaves[2 * i], leaves[2 * i + 1]]; - layer[i] = dep::std::hash::pedersen_hash(combined); + fn get_root(self) -> Field { + self.nodes[N-2] } - - calculate_subtree_8(layer.as_slice()) } -fn calculate_subtree_8(leaves : [Field]) -> Field { - assert(leaves.len() == 8, "number of leaves should be 8"); - let mut layer = [0; 4]; - for i in 0..layer.len() { - let combined = [leaves[2 * i], leaves[2 * i + 1]]; - layer[i] = dep::std::hash::pedersen_hash(combined); - } - - calculate_subtree_4(layer) -} -fn calculate_subtree_4(leaves : [Field]) -> Field { - assert(leaves.len() == 4, "number of leaves should be 4"); - - let mut layer = [0; 2]; - for i in 0..layer.len() { - let combined = [leaves[2 * i], leaves[2 * i + 1]]; - layer[i] = dep::std::hash::pedersen_hash(combined); - } - - calculate_subtree_2(layer.as_slice()) -} -fn calculate_subtree_2(leaves : [Field]) -> Field { - dep::std::hash::pedersen_hash(leaves) +pub fn calculate_subtree(leaves : [Field; N]) -> Field { + MerkleTree::new(leaves).get_root() } // These values are precomputed and we run tests to ensure that they @@ -125,6 +69,7 @@ pub fn calculate_empty_tree_root(depth : Field) -> Field { } } + #[test] fn test_merkle_root_interop_test() { // This is a test to ensure that we match the cpp implementation. From cdd1cf589461d8e964e3cc71ababbecf054ebf03 Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 3 Nov 2023 10:26:31 +0000 Subject: [PATCH 11/11] style: use shorter initialization for arrays --- .../src/crates/rollup-lib/src/merkle_tree.nr | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr index c603684d663..5a3e14f8e7f 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/merkle_tree.nr @@ -78,30 +78,30 @@ fn test_merkle_root_interop_test() { let root = calculate_subtree([1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4]); assert(0x17e8bb70a11d0c946345950879484d2f4f9fef397ff6adbfdec3baab2d41faab == root); - let empty_root = calculate_subtree([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); + let empty_root = calculate_subtree([0; 16]); assert(0x06e62084ee7b602fe9abc15632dda3269f56fb0c6e12519a2eb2ec897091919d == empty_root); } #[test] fn test_empty_subroot() { - let expected_empty_root_2 = calculate_subtree([0,0]); + let expected_empty_root_2 = calculate_subtree([0; 2]); assert(calculate_empty_tree_root(1) == expected_empty_root_2); - let expected_empty_root_4 = calculate_subtree([0,0,0,0]); + let expected_empty_root_4 = calculate_subtree([0; 4]); assert(calculate_empty_tree_root(2) == expected_empty_root_4); - let expected_empty_root_8 = calculate_subtree([0,0,0,0,0,0,0,0]); + let expected_empty_root_8 = calculate_subtree([0; 8]); assert(calculate_empty_tree_root(3) == expected_empty_root_8); - let expected_empty_root_16 = calculate_subtree([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); + let expected_empty_root_16 = calculate_subtree([0; 16]); assert(calculate_empty_tree_root(4) == expected_empty_root_16); - let expected_empty_root_32 = calculate_subtree([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); + let expected_empty_root_32 = calculate_subtree([0; 32]); assert(calculate_empty_tree_root(5) == expected_empty_root_32); - let expected_empty_root_64 = calculate_subtree([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); + let expected_empty_root_64 = calculate_subtree([0; 64]); assert(calculate_empty_tree_root(6) == expected_empty_root_64); - let expected_empty_root_128 = calculate_subtree([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); + let expected_empty_root_128 = calculate_subtree([0; 128]); assert(calculate_empty_tree_root(7) == expected_empty_root_128); } \ No newline at end of file