diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b627b2cd..dd930b3bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - [BREAKING] Refactored storage slots to be accessed by names instead of indices ([#1987](https://github.com/0xMiden/miden-base/pull/1987), [#2025](https://github.com/0xMiden/miden-base/pull/2025), [#2149](https://github.com/0xMiden/miden-base/pull/2149), [#2150](https://github.com/0xMiden/miden-base/pull/2150), [#2153](https://github.com/0xMiden/miden-base/pull/2153), [#2154](https://github.com/0xMiden/miden-base/pull/2154), [#2160](https://github.com/0xMiden/miden-base/pull/2160), [#2161](https://github.com/0xMiden/miden-base/pull/2161), [#2170](https://github.com/0xMiden/miden-base/pull/2170)). - [BREAKING] Allowed account components to share identical account code procedures ([#2164](https://github.com/0xMiden/miden-base/pull/2164)). - Add `AccountId::parse()` helper function to parse both hex and bech32 formats ([#2223](https://github.com/0xMiden/miden-base/pull/2223)). +- Add Keccak-based MMR frontier structure to the Agglayer library ([#2245](https://github.com/0xMiden/miden-base/pull/2245)). - Add `read_foreign_account_inputs()`, `read_vault_asset_witnesses()`, and `read_storage_map_witness()` for `TransactionInputs` ([#2246](https://github.com/0xMiden/miden-base/pull/2246)). - [BREAKING] Introduced `NoteAttachment` as part of `NoteMetadata` and remove `aux` and `execution_hint` ([#2249](https://github.com/0xMiden/miden-base/pull/2249), [#2252](https://github.com/0xMiden/miden-base/pull/2252), [#2260](https://github.com/0xMiden/miden-base/pull/2260), [#2268](https://github.com/0xMiden/miden-base/pull/2268), [#2279](https://github.com/0xMiden/miden-base/pull/2279)). - Added `AccountSchemaCommitment` component to expose account storage schema commitments ([#2253](https://github.com/0xMiden/miden-base/pull/2253)). diff --git a/Cargo.lock b/Cargo.lock index 7f5cdad93..69eb5f5ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1388,6 +1388,7 @@ dependencies = [ "miden-assembly", "miden-core", "miden-core-lib", + "miden-crypto", "miden-protocol", "miden-standards", "miden-utils-sync", diff --git a/crates/miden-agglayer/Cargo.toml b/crates/miden-agglayer/Cargo.toml index 019379d7f..7541b7ea8 100644 --- a/crates/miden-agglayer/Cargo.toml +++ b/crates/miden-agglayer/Cargo.toml @@ -35,6 +35,7 @@ fs-err = { workspace = true } miden-assembly = { workspace = true } miden-core = { workspace = true } miden-core-lib = { workspace = true } +miden-crypto = { workspace = true } miden-protocol = { features = ["testing"], workspace = true } miden-standards = { workspace = true } regex = { version = "1.11" } diff --git a/crates/miden-agglayer/asm/bridge/canonical_zeros.masm b/crates/miden-agglayer/asm/bridge/canonical_zeros.masm new file mode 100644 index 000000000..e693c4fa1 --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/canonical_zeros.masm @@ -0,0 +1,142 @@ +# This file is generated by build.rs, do not modify + +# This file contains the canonical zeros for the Keccak hash function. +# Zero of height `n` (ZERO_N) is the root of the binary tree of height `n` with leaves equal zero. +# +# Since the Keccak hash is represented by eight u32 values, each constant consists of two Words. + +const ZERO_0_L = [0, 0, 0, 0] +const ZERO_0_R = [0, 0, 0, 0] + +const ZERO_1_L = [3042949783, 3846789184, 2990541491, 2447652395] +const ZERO_1_R = [2532382527, 1151697986, 3453220726, 3056087725] + +const ZERO_2_L = [806175122, 2661877378, 3993486975, 3704028736] +const ZERO_2_R = [1186125340, 4132056164, 2406448277, 1360642484] + +const ZERO_3_L = [2243606276, 2319049635, 2778422344, 3686444836] +const ZERO_3_R = [836748766, 3055947948, 1063027030, 2746866977] + +const ZERO_4_L = [1150525734, 2360852476, 3881358125, 3462706719] +const ZERO_4_R = [224004420, 1513564138, 4058651434, 3010037733] + +const ZERO_5_L = [768598281, 293668224, 2114802790, 2680951561] +const ZERO_5_R = [523052921, 3386889228, 1344794057, 3206459406] + +const ZERO_6_L = [1746508463, 578821813, 283579568, 4134788524] +const ZERO_6_R = [756088757, 1715252246, 1087590535, 3173153928] + +const ZERO_7_L = [2205136186, 3475749318, 613780937, 1818541875] +const ZERO_7_R = [40140559, 91932979, 4234379492, 1459738623] + +const ZERO_8_L = [2941712185, 3321779339, 1227307046, 4069577285] +const ZERO_8_R = [611590243, 2128798138, 2473269631, 1607231384] + +const ZERO_9_L = [3763621903, 1154705673, 1903710296, 1972812290] +const ZERO_9_R = [4216691121, 4275626407, 3113795592, 3855940302] + +const ZERO_10_L = [2781069751, 774786966, 4112065289, 2182953470] +const ZERO_10_R = [3567589455, 861991663, 1356863200, 2134826233] + +const ZERO_11_L = [2465787000, 4149924453, 2720076317, 1467765009] +const ZERO_11_R = [1838648827, 866654147, 167150306, 1228583416] + +const ZERO_12_L = [2631517602, 171349786, 79648606, 4164671431] +const ZERO_12_R = [270336915, 2195882716, 3960096235, 3469119540] + +const ZERO_13_L = [3152187846, 1895984889, 2047814617, 1944734805] +const ZERO_13_R = [3551827087, 82830058, 326416580, 3649232833] + +const ZERO_14_L = [3435063385, 3598841737, 2762164692, 1894305546] +const ZERO_14_R = [3658789242, 3755895333, 49531590, 3618465628] + +const ZERO_15_L = [3525744215, 708101859, 2574387782, 3790037114] +const ZERO_15_R = [3700193742, 843132861, 3055060558, 2681109466] + +const ZERO_16_L = [530120689, 2718529082, 3981742412, 4194160956] +const ZERO_16_R = [4065390056, 824943129, 4207046226, 266679079] + +const ZERO_17_L = [2062522595, 650244466, 598998238, 1099357850] +const ZERO_17_R = [1543068721, 3603315816, 3833704967, 3367359457] + +const ZERO_18_L = [2692314236, 1072797208, 2923625471, 4157324078] +const ZERO_18_R = [746357617, 2400147060, 3144187786, 181284186] + +const ZERO_19_L = [2691355510, 1491476508, 3986541574, 2665487122] +const ZERO_19_R = [1032730592, 1039549588, 4164965877, 3056102068] + +const ZERO_20_L = [3803705507, 1732703975, 3478010394, 1535003327] +const ZERO_20_R = [4242360534, 719184416, 3062253412, 1167482566] + +const ZERO_21_L = [3655320222, 899251086, 3853444828, 1001466509] +const ZERO_21_R = [4045815225, 971767692, 1168258541, 2290434548] + +const ZERO_22_L = [2011403911, 3698331664, 3934089079, 946955861] +const ZERO_22_R = [3411854989, 1866109879, 418371072, 3692469338] + +const ZERO_23_L = [1390808632, 3168994683, 4234662665, 2053609922] +const ZERO_23_R = [2805567324, 2651248336, 696388782, 1078982733] + +const ZERO_24_L = [4011431532, 565969590, 1910056709, 4220355468] +const ZERO_24_R = [1681176506, 4292988995, 276516087, 2502281165] + +const ZERO_25_L = [2371989742, 3318538162, 999806777, 2066155765] +const ZERO_25_R = [1956437264, 2768897524, 1475191156, 3378167562] + +const ZERO_26_L = [3498569445, 3649628337, 1786802573, 2038831148] +const ZERO_26_R = [1678762243, 2385297319, 4030198639, 74763704] + +const ZERO_27_L = [516194684, 3360338824, 2165369292, 1916245748] +const ZERO_27_R = [3748991331, 1513828739, 3418759627, 1431735427] + +const ZERO_28_L = [787185022, 1571753335, 2366459736, 3067898230] +const ZERO_28_R = [79972070, 2975955312, 3165837101, 3722718822] + +const ZERO_29_L = [581144193, 3146618532, 1244629930, 2215341298] +const ZERO_29_R = [2551087773, 3876094376, 1909551909, 246581816] + +const ZERO_30_L = [903308566, 578217418, 2128594844, 1787682571] +const ZERO_30_R = [1078065138, 2904706143, 1223587258, 1350312851] + +const ZERO_31_L = [2840985724, 1653344606, 4049365781, 2389186238] +const ZERO_31_R = [3759582231, 2660540036, 1648733876, 2340505732] + +use ::miden::agglayer::mmr_frontier32_keccak::mem_store_double_word + +#! Inputs: [zeros_ptr] +#! Outputs: [] +pub proc load_zeros_to_memory + push.ZERO_0_L.ZERO_0_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_1_L.ZERO_1_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_2_L.ZERO_2_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_3_L.ZERO_3_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_4_L.ZERO_4_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_5_L.ZERO_5_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_6_L.ZERO_6_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_7_L.ZERO_7_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_8_L.ZERO_8_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_9_L.ZERO_9_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_10_L.ZERO_10_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_11_L.ZERO_11_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_12_L.ZERO_12_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_13_L.ZERO_13_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_14_L.ZERO_14_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_15_L.ZERO_15_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_16_L.ZERO_16_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_17_L.ZERO_17_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_18_L.ZERO_18_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_19_L.ZERO_19_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_20_L.ZERO_20_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_21_L.ZERO_21_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_22_L.ZERO_22_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_23_L.ZERO_23_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_24_L.ZERO_24_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_25_L.ZERO_25_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_26_L.ZERO_26_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_27_L.ZERO_27_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_28_L.ZERO_28_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_29_L.ZERO_29_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_30_L.ZERO_30_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_31_L.ZERO_31_R exec.mem_store_double_word dropw dropw add.8 + drop +end diff --git a/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm b/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm new file mode 100644 index 000000000..61d86cb99 --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm @@ -0,0 +1,344 @@ +use miden::core::crypto::hashes::keccak256 +use ::miden::agglayer::canonical_zeros::load_zeros_to_memory + +# An MMR Frontier is a data structure based on an MMR, which combines some features of an MMR and an +# SMT. +# +# # Basics & Terminology +# +# +# The main entity in this structure is a _frontier_: it is a set of roots of all individual trees in +# the MMR. Let's consider the tree below as an example. +# +# 7 +# / \ +# 3 6 10 +# / \ / \ / \ +# 1 2 4 5 8 9 11 +# +# The frontier will consist of nodes 7, 10, and 11, because they represent roots of each subtree and +# they are sufficient to compute the root of the entire MMR. If we add another node, the tree will +# become a full binary one and will look like so: +# +# 15 +# / \ +# / \ +# / \ +# 7 14 +# / \ / \ +# 3 6 10 13 +# / \ / \ / \ / \ +# 1 2 4 5 8 9 11 12 +# +# So in that case the frontier will consist of just one node 15. +# +# An MMR frontier consists of the current number of leaves in the range and the array containing the +# frontier. +# For the sake of simplicity, this array has a fixed length, equal to the maximum tree height. +# Indexes of 1's in the binary representation of the total leaves number show the indexes of the +# relevant frontier values in the frontier array for the current height. For example, if we have 10 +# leaves (1010 in binary representation), relevant frontier values will be stored at frontier[1] and +# frontier[3]. +# +# To compute the hash of two MMR nodes, a Keccak256 hash function is used. +# +# Each node in this MMR is represented by the Keccak256Digest. Notice that this hash is canonically +# represented on the stack by the 8 u32 values, or two words. So each node of the MMR will occupy +# two words on the stack, while being only a 256 bit value. +# +# Each state of the MMR frontier is represented by the root. This root is essentially equal to the +# root of the SMT which has the height equal to the maximum height of the current MMR (for this +# implementation this maximum height is set to 32), and the leaves equal to the MMR frontier leaves +# plus the "zero hash" leaves (Keccak256::hash(&[0u8; 32])) for all other ones. +# +# # Layout +# +# The memory layout of the MMR frontier looks like so: +# +# [num_leaves, 0, 0, 0, [FRONTIER_VALUE_DW]] +# +# Where: +# - num_leaves is the number of leaves in the MMR before adding the new leaf. +# - [FRONTIER_VALUE_DW] is an array containing the double words which represent the frontier MMR +# nodes. Notice that the index of a frontier value in this array represent its height in the tree. +# +# Zero hashes which are used during the root computation are stored in the local memory of the +# `append_and_update_frontier` procedure. + +# ERRORS +# ================================================================================================= + +const ERR_MMR_FRONTIER_LEAVES_NUM_EXCEED_LIMIT = "number of leaves in the MMR of the MMR Frontier would exceed 4294967295 (2^32 - 1)" + +# CONSTANTS +# ================================================================================================= + +# The maximum number of leaves which could be added to the MMR. +# +# If the height is 32, the leaves num will be equal to 4294967295 (2**32 - 1) +const MAX_LEAVES_NUM = 4294967295 +const MAX_LEAVES_MINUS_1 = 4294967294 + +# The total height of the full MMR tree, whose root represents the commitment to the current +# frontier. +const TREE_HEIGHT = 32 + +# The number of the stack elements which one node occupy. +const NODE_SIZE = 8 + +# The offset of the number of leaves in the current MMR state. +const NUM_LEAVES_OFFSET = 0 + +# The offset of the array of the frontier nodes of respective heights. +const FRONTIER_OFFSET = 4 # 32 double words, 256 felts in total + +# The offset of the first half of the current Keccak256 hash value in the local memory of the +# `append_and_update_frontier` procedure. +const CUR_HASH_LO_LOCAL = 0 + +# The offset of the second half of the current Keccak256 hash value in the local memory of the +# `append_and_update_frontier` procedure. +const CUR_HASH_HI_LOCAL = 4 + +# The offset of the canonical zeros stored in the local memory of the `append_and_update_frontier` +# procedure. +const CANONICAL_ZEROES_LOCAL = 8 + +# PUBLIC API +# ================================================================================================= + +#! Updates the existing frontier with the new leaf, returns a new leaf count and a new MMR root. +#! +#! The memory layout at the `mmr_frontier_ptr` is expected to be: +#! [num_leaves, [[FRONTIER_NODE_LO, FRONTIER_NODE_HI]; 32]] +#! Empty uninitialized memory is a valid state for the frontier in the case where there are no +#! leaves. +#! +#! The layout of the local memory of this `append_and_update_frontier` procedure looks like so: +#! [CUR_HASH_LO, CUR_HASH_HI, [[CANONICAL_ZERO_LO, CANONICAL_ZERO_HI]; 32]] +#! So the first 8 felt values is occupied by the current Keccak256 hash, and next 32 * 8 felt values +#! is occupied by the canonical zeros, 8 values each, 32 zeros total. +#! +#! Inputs: [NEW_LEAF_LO, NEW_LEAF_HI, mmr_frontier_ptr] +#! Outputs: [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] +#! +#! Where: +#! - [NEW_LEAF_LO, NEW_LEAF_HI] is the new leaf, represented as Keccak256 hash, which will be added +#! to the MMR. +#! - mmr_frontier_ptr is the pointer to the memory where the MMR Frontier structure is located. +#! - [NEW_ROOT_LO, NEW_ROOT_HI] is the new root of the MMR, represented as Keccak256 hash. +#! - new_leaf_count is the number of leaves in the MMR after the new leaf was added. +#! +#! Panics if: +#! - The number of leaves in the MMR has reached the maximum limit of 2^32. +@locals(264) # new_leaf/curr_hash + canonical_zeros +pub proc append_and_update_frontier + # set CUR_HASH = NEW_LEAF and store to local memory + loc_storew_be.CUR_HASH_LO_LOCAL dropw + loc_storew_be.CUR_HASH_HI_LOCAL dropw + # => [mmr_frontier_ptr] + + # get the current leaves number + dup add.NUM_LEAVES_OFFSET mem_load + # => [num_leaves, mmr_frontier_ptr] + + # make sure that the MMR is not full yet and we still can store the new leaf + # the MMR is full when the number of leaves is equal to 2^TREE_HEIGHT - 1 (as per the + # Solidity implementation), so the last call to this procedure will be when the number of + # leaves would be equal to 2^32 - 2. + # first assert that the number of leaves is a valid u32, else the u32lt assertion is undefined + u32assert.err=ERR_MMR_FRONTIER_LEAVES_NUM_EXCEED_LIMIT + dup u32lte.MAX_LEAVES_MINUS_1 assert.err=ERR_MMR_FRONTIER_LEAVES_NUM_EXCEED_LIMIT + # => [num_leaves, mmr_frontier_ptr] + + # get the memory pointer where the canonical zeros will be stored + locaddr.CANONICAL_ZEROES_LOCAL + # => [zeros_ptr, num_leaves, mmr_frontier_ptr] + + # load the canonical zeros into the memory + exec.load_zeros_to_memory + # => [num_leaves, mmr_frontier_ptr] + + # update the leaves number and store it into the memory + dup add.1 dup.2 add.NUM_LEAVES_OFFSET + # => [num_leaves_ptr, num_leaves+1, num_leaves, mmr_frontier_ptr] + + mem_store + # => [num_leaves, mmr_frontier_ptr] + + # iterate `TREE_HEIGHT` times to get the root of the tree + # + # iter_counter in that case will show the current tree height + push.0 push.1 + # => [loop_flag=1, iter_counter=0, num_leaves, mmr_frontier_ptr] + + while.true + # => [curr_tree_height, num_leaves, mmr_frontier_ptr] + + # get the pointer to the frontier node of the current height + # + # notice that the initial state of the frontier array is zeros + dup.2 add.FRONTIER_OFFSET dup.1 mul.NODE_SIZE add + # => [frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mmr_frontier_ptr] + + # determine whether the last `num_leaves` bit is 1 (is `num_leaves` odd) + dup.2 u32and.1 + # => [ + # is_odd, frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mmr_frontier_ptr + # ] + + if.true + # => [frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mmr_frontier_ptr] + # + # this height already had a subtree root stored in frontier[curr_tree_height], merge + # into parent. + exec.mem_load_double_word + # => [ + # FRONTIER[curr_tree_height]_LO, FRONTIER[curr_tree_height]_HI, curr_tree_height, + # num_leaves, mmr_frontier_ptr + # ] + + # load the current hash from the local memory back to the stack + # + # in the first iteration the current hash will be equal to the new node + padw loc_loadw_be.CUR_HASH_HI_LOCAL + padw loc_loadw_be.CUR_HASH_LO_LOCAL + swapdw + # => [ + # FRONTIER[curr_tree_height]_LO, FRONTIER[curr_tree_height]_HI, CUR_HASH_LO, + # CUR_HASH_HI, curr_tree_height, num_leaves, mmr_frontier_ptr + # ] + + # merge the frontier node of this height with the current hash to get the current hash + # of the next height (merge(frontier[h], cur)) + exec.keccak256::merge + # => [CUR_HASH_LO', CUR_HASH_HI', curr_tree_height, num_leaves, mmr_frontier_ptr] + + # store the current hash of the next height back to the local memory + loc_storew_be.CUR_HASH_LO_LOCAL dropw + loc_storew_be.CUR_HASH_HI_LOCAL dropw + # => [curr_tree_height, num_leaves, mmr_frontier_ptr] + else + # => [frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mmr_frontier_ptr] + # + # this height wasn't "occupied" yet: store the current hash as the subtree root + # (frontier node) at height `curr_tree_height` + padw loc_loadw_be.CUR_HASH_HI_LOCAL + padw loc_loadw_be.CUR_HASH_LO_LOCAL + # => [ + # CUR_HASH_LO, CUR_HASH_HI, frontier[curr_tree_height]_ptr, curr_tree_height, + # num_leaves, mmr_frontier_ptr + # ] + + # store the CUR_HASH to the frontier[curr_tree_height]_ptr + exec.mem_store_double_word movup.8 drop + # => [CUR_HASH_LO, CUR_HASH_HI, curr_tree_height, num_leaves, mmr_frontier_ptr] + + # get the pointer to the canonical zero node of the current height + locaddr.CANONICAL_ZEROES_LOCAL dup.9 mul.NODE_SIZE add + # => [ + # zeros[curr_tree_height], CUR_HASH_LO, CUR_HASH_HI, curr_tree_height, num_leaves, + # mmr_frontier_ptr + # ] + + # load the zero node to the stack + exec.mem_load_double_word swapdw + # => [ + # CUR_HASH_LO, CUR_HASH_HI, ZERO_H_LO, ZERO_H_HI, curr_tree_height, num_leaves, + # mmr_frontier_ptr + # ] + + # merge the current hash with the zero node of this height to get the current hash of + # the next height (merge(cur, zeroes[h])) + exec.keccak256::merge + # => [CUR_HASH_LO', CUR_HASH_HI', curr_tree_height, num_leaves, mmr_frontier_ptr] + + # store the current hash of the next height back to the local memory + loc_storew_be.CUR_HASH_LO_LOCAL dropw + loc_storew_be.CUR_HASH_HI_LOCAL dropw + # => [curr_tree_height, num_leaves, mmr_frontier_ptr] + end + # => [curr_tree_height, num_leaves, mmr_frontier_ptr] + + # update the current tree height + add.1 + # => [curr_tree_height+1, num_leaves, mmr_frontier_ptr] + + # update the `num_leaves` (shift it right by 1 bit) + swap u32shr.1 swap + # => [curr_tree_height+1, num_leaves>>1, mmr_frontier_ptr] + + # compute the cycle flag + dup neq.TREE_HEIGHT + # => [loop_flag, curr_tree_height+1, num_leaves>>1, mmr_frontier_ptr] + end + # => [curr_tree_height=TREE_HEIGHT, num_leaves=0, mmr_frontier_ptr] + + # clean the stack + drop drop + # => [mmr_frontier_ptr] + + # load the final number of leaves onto the stack + add.NUM_LEAVES_OFFSET mem_load + # => [new_leaf_count] + + # The current (final) hash represents the root of the whole tree. + # + # Notice that there is no need to update the frontier[tree_height] value, which in theory could + # represent the frontier in case the tree is full. The frontier nodes are used only for the + # computation of the next height hash, but if the tree is full, there is no next hash to + # compute. + + # load the final hash (which is also the root of the tree) + padw loc_loadw_be.CUR_HASH_HI_LOCAL + padw loc_loadw_be.CUR_HASH_LO_LOCAL + # => [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] +end + +# HELPER PROCEDURES +# ================================================================================================= + +#! Stores the canonical zeros from the advice map to the memory at the provided address. +#! +#! Inputs: [zeros_ptr] +#! Outputs: [] +proc store_canonical_zeros + # prepare the stack for the adv_pipe instruction + padw padw padw + # => [PAD, PAD, PAD, zeros_ptr] + + # TODO: use constant once constant usage will be implemented + repeat.32 + adv_pipe + # => [ZERO_I_L, ZERO_I_R, PAD, zeros_ptr+8] + end + # => [ZERO_31_L, ZERO_31_R, PAD, zeros_ptr+256] + + # clean the stack + dropw dropw dropw drop + # => [] +end + +#! Stores two words to the provided global memory address. +#! +#! Inputs: [WORD_1, WORD_2, ptr] +#! Outputs: [WORD_1, WORD_2, ptr] +pub proc mem_store_double_word + dup.8 mem_storew_be swapw + # => [WORD_2, WORD_1, ptr] + + dup.8 add.4 mem_storew_be swapw + # => [WORD_1, WORD_2, ptr] +end + +#! Loads two words from the provided global memory address. +#! +#! Inputs: [ptr] +#! Outputs: [WORD_1, WORD_2] +proc mem_load_double_word + padw dup.4 add.4 mem_loadw_be + # => [WORD_2, ptr] + + padw movup.8 mem_loadw_be + # => [WORD_1, WORD_2] +end diff --git a/crates/miden-agglayer/build.rs b/crates/miden-agglayer/build.rs index fbd2cd06e..d57b3c3ed 100644 --- a/crates/miden-agglayer/build.rs +++ b/crates/miden-agglayer/build.rs @@ -5,6 +5,7 @@ use fs_err as fs; use miden_assembly::diagnostics::{IntoDiagnostic, Result, WrapErr}; use miden_assembly::utils::Serializable; use miden_assembly::{Assembler, Library, Report}; +use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest}; use miden_protocol::transaction::TransactionKernel; // CONSTANTS @@ -38,6 +39,10 @@ fn main() -> Result<()> { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let build_dir = env::var("OUT_DIR").unwrap(); let src = Path::new(&crate_dir).join(ASM_DIR); + + // generate canonical zeros in `asm/bridge/canonical_zeros.masm` + generate_canonical_zeros(&src.join(ASM_BRIDGE_DIR))?; + let dst = Path::new(&build_dir).to_path_buf(); shared::copy_directory(src, &dst, ASM_DIR)?; @@ -134,7 +139,7 @@ fn compile_note_scripts( // COMPILE ACCOUNT COMPONENTS (DEPRECATED) // ================================================================================================ -/// Compiles the bridge components in `source_dir` into MASL libraries and stores the compiled +/// Compiles the agglayer library in `source_dir` into MASL libraries and stores the compiled /// files in `target_dir`. /// /// NOTE: This function is deprecated and replaced by compile_agglayer_lib @@ -230,6 +235,77 @@ fn generate_error_constants(asm_source_dir: &Path) -> Result<()> { Ok(()) } +// CANONICAL ZEROS FILE GENERATION +// ================================================================================================ + +fn generate_canonical_zeros(target_dir: &Path) -> Result<()> { + if !BUILD_GENERATED_FILES_IN_SRC { + return Ok(()); + } + + const TREE_HEIGHT: u8 = 32; + + let mut zeros_by_height = Vec::with_capacity(TREE_HEIGHT as usize); + + // Push the zero of height 0 to the zeros vec. This is done separately because the zero of + // height 0 is just a plain zero array ([0u8; 32]), it doesn't require to perform any hashing. + zeros_by_height.push(Keccak256Digest::default()); + + // Compute the canonical zeros for each height from 1 to TREE_HEIGHT + // Zero of height `n` is computed as: `ZERO_N = Keccak256::merge(ZERO_{N-1}, ZERO_{N-1})` + for _ in 1..TREE_HEIGHT { + let current_height_zero = + Keccak256::merge(&[*zeros_by_height.last().unwrap(), *zeros_by_height.last().unwrap()]); + zeros_by_height.push(current_height_zero); + } + + // convert the keccak digest into the sequence of u32 values and create two word constants from + // them to represent the hash + let mut zero_constants = String::from( + "# This file is generated by build.rs, do not modify\n +# This file contains the canonical zeros for the Keccak hash function. +# Zero of height `n` (ZERO_N) is the root of the binary tree of height `n` with leaves equal zero. +# +# Since the Keccak hash is represented by eight u32 values, each constant consists of two Words.\n", + ); + + for (height, zero) in zeros_by_height.iter().enumerate() { + let zero_as_u32_vec = zero + .chunks(4) + .map(|chunk_u32| u32::from_le_bytes(chunk_u32.try_into().unwrap()).to_string()) + .rev() + .collect::>(); + + zero_constants.push_str(&format!( + "\nconst ZERO_{height}_L = [{}]\n", + zero_as_u32_vec[..4].join(", ") + )); + zero_constants + .push_str(&format!("const ZERO_{height}_R = [{}]\n", zero_as_u32_vec[4..].join(", "))); + } + + // remove once CANONICAL_ZEROS advice map is available + zero_constants.push_str( + " +use ::miden::agglayer::mmr_frontier32_keccak::mem_store_double_word + +#! Inputs: [zeros_ptr] +#! Outputs: [] +pub proc load_zeros_to_memory\n", + ); + + for zero_index in 0..32 { + zero_constants.push_str(&format!("\tpush.ZERO_{zero_index}_L.ZERO_{zero_index}_R exec.mem_store_double_word dropw dropw add.8\n")); + } + + zero_constants.push_str("\tdrop\nend\n"); + + // write the resulting masm content into the file + fs::write(target_dir.join("canonical_zeros.masm"), zero_constants).into_diagnostic()?; + + Ok(()) +} + /// This module should be kept in sync with the copy in miden-protocol's and miden-standards' /// build.rs. mod shared { diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index efa9275de..89aa33a16 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -26,6 +26,9 @@ pub const ERR_FELT_OUT_OF_FIELD: MasmError = MasmError::from_static_str("combine /// Error Message: "invalid claim proof" pub const ERR_INVALID_CLAIM_PROOF: MasmError = MasmError::from_static_str("invalid claim proof"); +/// Error Message: "number of leaves in the MMR of the MMR Frontier would exceed 4294967295 (2^32 - 1)" +pub const ERR_MMR_FRONTIER_LEAVES_NUM_EXCEED_LIMIT: MasmError = MasmError::from_static_str("number of leaves in the MMR of the MMR Frontier would exceed 4294967295 (2^32 - 1)"); + /// Error Message: "address limb is not u32" pub const ERR_NOT_U32: MasmError = MasmError::from_static_str("address limb is not u32"); diff --git a/crates/miden-testing/Cargo.toml b/crates/miden-testing/Cargo.toml index 23bea9875..7c5314be9 100644 --- a/crates/miden-testing/Cargo.toml +++ b/crates/miden-testing/Cargo.toml @@ -27,6 +27,7 @@ miden-tx-batch-prover = { features = ["testing"], workspace = true } # Miden dependencies miden-assembly = { workspace = true } miden-core-lib = { workspace = true } +miden-crypto = { workspace = true } miden-processor = { workspace = true } # External dependencies diff --git a/crates/miden-testing/tests/agglayer/mmr_frontier.rs b/crates/miden-testing/tests/agglayer/mmr_frontier.rs new file mode 100644 index 000000000..b4e800703 --- /dev/null +++ b/crates/miden-testing/tests/agglayer/mmr_frontier.rs @@ -0,0 +1,180 @@ +use alloc::format; +use alloc::string::ToString; + +use miden_agglayer::agglayer_library; +use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest}; +use miden_protocol::Felt; +use miden_protocol::utils::sync::LazyLock; +use miden_standards::code_builder::CodeBuilder; +use miden_testing::TransactionContextBuilder; + +// KECCAK MMR FRONTIER +// ================================================================================================ + +static CANONICAL_ZEROS_32: LazyLock> = LazyLock::new(|| { + let mut zeros_by_height = Vec::with_capacity(32); + + // Push the zero of height 0 to the zeros vec. This is done separately because the zero of + // height 0 is just a plain zero array ([0u8; 32]), it doesn't require to perform any hashing. + zeros_by_height.push(Keccak256Digest::default()); + + // Compute the canonical zeros for each height from 1 to 32 + // Zero of height `n` is computed as: `ZERO_N = Keccak256::merge(ZERO_{N-1}, ZERO_{N-1})` + for _ in 1..32 { + let last_zero = zeros_by_height.last().expect("zeros vec should have at least one value"); + let current_height_zero = Keccak256::merge(&[*last_zero, *last_zero]); + zeros_by_height.push(current_height_zero); + } + + zeros_by_height +}); + +struct KeccakMmrFrontier32 { + num_leaves: u32, + frontier: [Keccak256Digest; TREE_HEIGHT], +} + +impl KeccakMmrFrontier32 { + pub fn new() -> Self { + Self { + num_leaves: 0, + frontier: [Keccak256Digest::default(); TREE_HEIGHT], + } + } + + pub fn append_and_update_frontier(&mut self, new_leaf: Keccak256Digest) -> Keccak256Digest { + let mut curr_hash = new_leaf; + let mut idx = self.num_leaves; + self.num_leaves += 1; + + for height in 0..TREE_HEIGHT { + if (idx & 1) == 0 { + // This height wasn't "occupied" yet: store cur as the subtree root at height h. + self.frontier[height] = curr_hash; + + // Pair it with the canonical zero subtree on the right at this height. + curr_hash = Keccak256::merge(&[curr_hash, CANONICAL_ZEROS_32[height]]); + } else { + // This height already had a subtree root stored in frontier[h], merge into parent. + curr_hash = Keccak256::merge(&[self.frontier[height], curr_hash]) + } + + idx >>= 1; + } + + // curr_hash at this point is equal to the root of the full tree + curr_hash + } +} + +// TESTS +// ================================================================================================ + +#[tokio::test] +async fn test_append_and_update_frontier() -> anyhow::Result<()> { + let mut mmr_frontier = KeccakMmrFrontier32::<32>::new(); + + let mut source = "use miden::agglayer::mmr_frontier32_keccak begin".to_string(); + + for round in 0..32 { + // construct the leaf from the hex representation of the round number + let leaf = Keccak256Digest::try_from(format!("{:#066x}", round).as_str()).unwrap(); + let root = mmr_frontier.append_and_update_frontier(leaf); + let num_leaves = mmr_frontier.num_leaves; + + source.push_str(&leaf_assertion_code(leaf, root, num_leaves)); + } + + source.push_str("end"); + + let tx_script = CodeBuilder::new() + .with_statically_linked_library(&agglayer_library())? + .compile_tx_script(source)?; + + TransactionContextBuilder::with_existing_mock_account() + .tx_script(tx_script.clone()) + .build()? + .execute() + .await?; + + Ok(()) +} + +#[tokio::test] +async fn test_check_empty_mmr_root() -> anyhow::Result<()> { + let zero_leaf = Keccak256Digest::default(); + let zero_31 = *CANONICAL_ZEROS_32.get(31).expect("zeros should have 32 values total"); + let empty_mmr_root = Keccak256::merge(&[zero_31, zero_31]); + + let mut source = "use miden::agglayer::mmr_frontier32_keccak begin".to_string(); + + for round in 1..=32 { + // check that pushing the zero leaves into the MMR doesn't change its root + source.push_str(&leaf_assertion_code(zero_leaf, empty_mmr_root, round)); + } + + source.push_str("end"); + + let tx_script = CodeBuilder::new() + .with_statically_linked_library(&agglayer_library())? + .compile_tx_script(source)?; + + TransactionContextBuilder::with_existing_mock_account() + .tx_script(tx_script.clone()) + .build()? + .execute() + .await?; + + Ok(()) +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Transforms the `[Keccak256Digest]` into two word strings: (`a, b, c, d`, `e, f, g, h`) +fn keccak_digest_to_word_strings(digest: Keccak256Digest) -> (String, String) { + let double_word = (*digest) + .chunks(4) + .map(|chunk| Felt::from(u32::from_le_bytes(chunk.try_into().unwrap())).to_string()) + .rev() + .collect::>(); + + (double_word[0..4].join(", "), double_word[4..8].join(", ")) +} + +fn leaf_assertion_code( + leaf: Keccak256Digest, + expected_root: Keccak256Digest, + num_leaves: u32, +) -> String { + let (leaf_hi, leaf_lo) = keccak_digest_to_word_strings(leaf); + let (root_hi, root_lo) = keccak_digest_to_word_strings(expected_root); + + format!( + r#" + # load the provided leaf onto the stack + push.[{leaf_hi}] + push.[{leaf_lo}] + + # add this leaf to the MMR frontier + exec.mmr_frontier32_keccak::append_and_update_frontier + # => [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] + + # assert the root correctness after the first leaf was added + push.[{root_lo}] + push.[{root_hi}] + movdnw.3 + # => [EXPECTED_ROOT_LO, NEW_ROOT_LO, NEW_ROOT_HI, EXPECTED_ROOT_HI, new_leaf_count] + + assert_eqw.err="MMR root (LO) is incorrect" + # => [NEW_ROOT_HI, EXPECTED_ROOT_HI, new_leaf_count] + + assert_eqw.err="MMR root (HI) is incorrect" + # => [new_leaf_count] + + # assert the new number of leaves + push.{num_leaves} + assert_eq.err="new leaf count is incorrect" + "# + ) +} diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index 65269c8c4..44e687a15 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -2,5 +2,6 @@ pub mod asset_conversion; mod bridge_in; mod bridge_out; mod crypto_utils; +mod mmr_frontier; mod solidity_miden_address_conversion; pub mod test_utils;