Skip to content

Commit

Permalink
feat: Add root rollup circuit (#3217)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: sirasistant <sirasistant@gmail.com>
  • Loading branch information
kevaundray and sirasistant authored Nov 3, 2023
1 parent 410cae3 commit fb4f7af
Show file tree
Hide file tree
Showing 7 changed files with 317 additions and 6 deletions.
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

0 comments on commit fb4f7af

Please sign in to comment.