Skip to content
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

feat: Add root rollup circuit #3217

Merged
merged 11 commits into from
Nov 3, 2023
Merged
29 changes: 29 additions & 0 deletions circuits/cpp/src/aztec3/circuits/rollup/root/.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<fr, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP> 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
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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<N>(
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}
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ mod merge;
mod root;

mod components;

mod hash;

mod merkle_tree;
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
struct MerkleTree<N> {
leaves: [Field; N],
nodes: [Field; N],
}

impl<N> MerkleTree<N> {
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,
}
}

fn get_root(self) -> Field {
self.nodes[N-2]
}
}

pub fn calculate_subtree<N>(leaves : [Field; N]) -> Field {
MerkleTree::new(leaves).get_root()
}

// 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.
pub 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; 16]);
assert(0x06e62084ee7b602fe9abc15632dda3269f56fb0c6e12519a2eb2ec897091919d == empty_root);
}

#[test]
fn test_empty_subroot() {
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; 4]);
assert(calculate_empty_tree_root(2) == expected_empty_root_4);

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; 16]);
assert(calculate_empty_tree_root(4) == expected_empty_root_16);

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; 64]);
assert(calculate_empty_tree_root(6) == expected_empty_root_64);

let expected_empty_root_128 = calculate_subtree([0; 128]);
assert(calculate_empty_tree_root(7) == expected_empty_root_128);
}
131 changes: 128 additions & 3 deletions yarn-project/noir-protocol-circuits/src/crates/rollup-lib/src/root.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<N>(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
Expand All @@ -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<N>(leaf : Field, leaf_index : Field, sibling_path : [Field; N]) -> Field {
pub fn root_from_sibling_path<N>(leaf : Field, leaf_index : Field, sibling_path : [Field; N]) -> Field {
let mut node = leaf;
let indices = leaf_index.to_le_bits(N);

Expand Down