diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f3391221..b8896924ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,8 @@ ### Changes +- [BREAKING] Renamed `MMR Frontier` to `Merkle Tree Frontier (MTF)`, module was renamed from `mmr_frontier32_keccak` to `merkle_tree_frontier` ([#2642](https://github.com/0xMiden/protocol/pull/2642)). +- [BREAKING] Separated `EthAddress` (plain 20-byte Ethereum address) and `EthEmbeddedAccountId` (Miden AccountId encoded as Ethereum address) into distinct types, replacing the single `EthAddressFormat` struct. ([#2622](https://github.com/0xMiden/protocol/pull/2622)). - Migrated to miden-vm v0.22 and miden-crypto v0.23 ([#2644](https://github.com/0xMiden/protocol/pull/2644)). - [BREAKING] Renamed `AccountComponent::get_procedures()` to `procedures()`, returning `impl Iterator` ([#2597](https://github.com/0xMiden/protocol/pull/2597)). - [BREAKING] Removed `NoteAssets::add_asset`; `OutputNoteBuilder` now accumulates assets in a `Vec` and computes the commitment only when `build()` is called, avoiding rehashing on every asset addition. ([#2577](https://github.com/0xMiden/protocol/pull/2577)) diff --git a/Makefile b/Makefile index 019bbd2bf9..03d5211d73 100644 --- a/Makefile +++ b/Makefile @@ -145,7 +145,7 @@ build-no-std-testing: ## Build without the standard library. Includes the `testi # --- test vectors -------------------------------------------------------------------------------- .PHONY: generate-solidity-test-vectors -generate-solidity-test-vectors: ## Regenerate Solidity MMR test vectors using Foundry +generate-solidity-test-vectors: ## Regenerate Solidity test vectors using Foundry cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateVectors cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateCanonicalZeros cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateVerificationProofData diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 924330d031..e80ab2d82c 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -12,12 +12,6 @@ implementation are called out inline with `TODO (Future)` markers. - *Word* = 4 field elements (felts), each < p (Goldilocks prime 2^64 - 2^32 + 1). - *Felt* = a single Goldilocks field element. -- Word values in this spec use **element-index notation** matching Rust's - `Word::new([e0, e1, e2, e3])`. MASM doc comments use **stack notation** (top-first), - which reverses the order: stack `[a, b, c, d]` = Word `[d, c, b, a]`. -- Procedure input/output signatures use **stack notation** (top-first), matching the - MASM doc comments. -- `TODO (Future)` marks non-implemented design points. --- @@ -39,7 +33,8 @@ implementation are called out inline with `TODO (Future)` markers. | B2AGG (reclaim) | Any user -- not restricted | Original sender only -- **enforced**: script checks `sender == consuming account` | | CONFIG_AGG_BRIDGE | Bridge admin only -- **enforced** by `bridge_config::register_faucet` procedure | Bridge account -- **enforced** via `NetworkAccountTarget` attachment | | UPDATE_GER | GER manager only -- **enforced** by `bridge_config::update_ger` procedure | Bridge account -- **enforced** via `NetworkAccountTarget` attachment | -| CLAIM | Anyone -- not restricted | Target faucet only -- **enforced** via `NetworkAccountTarget` attachment | +| CLAIM | Anyone -- not restricted | Bridge account -- **enforced** via `NetworkAccountTarget` attachment | +| MINT | Bridge account only -- **enforced** by faucet's `owner_only` mint policy via `Ownable2Step` (asserts note sender is the faucet's owner, i.e. the bridge) | Target faucet only -- **enforced** via `NetworkAccountTarget` attachment | --- @@ -52,6 +47,7 @@ which is a thin wrapper that re-exports procedures from the `agglayer` library m - `bridge_config::register_faucet` - `bridge_config::update_ger` +- `bridge_in::claim` - `bridge_out::bridge_out` The underlying library code lives in `asm/agglayer/bridge/` with supporting modules in @@ -72,7 +68,7 @@ Bridges an asset out of Miden into the AggLayer: 1. Validates the asset's faucet is registered in the faucet registry. 2. FPIs to `agglayer_faucet::asset_to_origin_asset` on the faucet account to obtain the scaled U256 amount, origin token address, and origin network. 3. Builds a leaf-data structure in memory (leaf type, origin network, origin token address, destination network, destination address, amount, metadata hash). -4. Computes the Keccak-256 leaf value and appends it to the Local Exit Tree (MMR frontier). +4. Computes the Keccak-256 leaf value and appends it to the Local Exit Tree. 5. Creates a public `BURN` note targeting the faucet via a `NetworkAccountTarget` attachment. #### `bridge_config::register_faucet` @@ -80,15 +76,19 @@ Bridges an asset out of Miden into the AggLayer: | | | |-|-| | **Invocation** | `call` | -| **Inputs** | `[faucet_id_prefix, faucet_id_suffix, pad(14)]` | +| **Inputs** | `[origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(9)]` | | **Outputs** | `[pad(16)]` | | **Context** | Consuming a `CONFIG_AGG_BRIDGE` note on the bridge account | | **Panics** | Note sender is not the bridge admin | Asserts the note sender matches the bridge admin stored in -`agglayer::bridge::admin_account_id`, then writes -`[0, 0, faucet_id_suffix, faucet_id_prefix] -> [1, 0, 0, 0]` into the -`faucet_registry_map` map slot. +`agglayer::bridge::admin_account_id`, then performs a two-step registration: + +1. Writes `[0, 0, faucet_id_suffix, faucet_id_prefix] -> [1, 0, 0, 0]` into the + `faucet_registry_map` map slot. +2. Hashes `origin_token_addr` (5 felts) using `Poseidon2::hash_elements` and writes + `hash(origin_token_addr) -> [0, 0, faucet_id_suffix, faucet_id_prefix]` into the + `token_registry_map` map slot. #### `bridge_config::update_ger` @@ -102,39 +102,57 @@ Asserts the note sender matches the bridge admin stored in Asserts the note sender matches the GER manager stored in `agglayer::bridge::ger_manager_account_id`, then computes -`KEY = rpo256::merge(GER_UPPER, GER_LOWER)` and stores +`KEY = poseidon2::merge(GER_LOWER, GER_UPPER)` and stores `KEY -> [1, 0, 0, 0]` in the `ger_map` map slot. This marks the GER as "known". -#### `bridge_in::verify_leaf_bridge` -TODO ([#2624](https://github.com/0xMiden/protocol/issues/2624)): document new CLAIM note flow. +#### `bridge_in::claim` | | | |-|-| -| **Invocation** | `call` (invoked via FPI from the faucet) | -| **Inputs** | `[LEAF_DATA_KEY, PROOF_DATA_KEY, pad(8)]` on the operand stack; proof data and leaf data in the advice map | +| **Invocation** | `call` | +| **Inputs** | `[PROOF_DATA_KEY, LEAF_DATA_KEY, faucet_mint_amount, pad(7)]` on the operand stack; proof data and leaf data in the advice map keyed by `PROOF_DATA_KEY` and `LEAF_DATA_KEY` respectively | | **Outputs** | `[pad(16)]` | -| **Context** | FPI target -- called by the faucet during `CLAIM` consumption | -| **Panics** | GER not known; global index not mainnet; rollup index non-zero; Merkle proof verification failed | - -Verifies a bridge-in claim: - -1. Retrieves leaf data from the advice map, computes the Keccak-256 leaf value. -2. Retrieves proof data from the advice map: SMT proofs, global index, exit roots. -3. Computes the GER from `mainnet_exit_root` and `rollup_exit_root`, asserts it is in - the known GER set. -4. Extracts the leaf index from the global index (must be mainnet, rollup index = 0). (TODO (Future): rollup indices are not processed yet [#2394](https://github.com/0xMiden/protocol/issues/2394)). -5. Verifies the Merkle proof: leaf value at `leaf_index` against `mainnet_exit_root`. +| **Context** | Consuming a `CLAIM` note on the bridge account | +| **Panics** | GER not known; global index invalid; Merkle proof verification failed; origin token address not in token registry; claim already spent; amount conversion mismatch | + +Validates a bridge-in claim and creates a MINT note targeting the faucet: + +1. Pipes proof data and leaf data from the advice map into memory, verifying preimage + integrity. +2. Extracts the destination account ID from the leaf data's destination address + (via `eth_address::to_account_id`). +3. Validates the Merkle proof via `verify_leaf_bridge`: computes the leaf + value from leaf data, computes the GER from mainnet + rollup exit roots, asserts + GER is known, processes global index (mainnet or rollup), verifies Merkle proof. + For mainnet: single proof against `mainnet_exit_root`. For rollup: two-level proof + (leaf against `local_exit_root`, then `local_exit_root` against `rollup_exit_root`, though the first check is implicit). +4. Updates the claimed global index (CGI) chain hash: + `NEW_CGI = Keccak256(OLD_CGI, Keccak256(GLOBAL_INDEX, LEAF_VALUE))`. +5. Computes and checks the claim nullifier + `Poseidon2::hash_elements(leaf_index, source_bridge_network)` to prevent + double-claiming. For mainnet deposits, `source_bridge_network = 0`. For rollup + deposits, `source_bridge_network = rollup_index + 1`. +6. Looks up the faucet account ID from the origin token address via + `bridge_config::lookup_faucet_by_token_address`. +7. Verifies the `faucet_mint_amount` against the leaf data's U256 amount and the + faucet's scale factor (via FPI to `agglayer_faucet::get_scale`), using + `asset_conversion::verify_u256_to_native_amount_conversion`. +8. Builds a MINT output note targeting the faucet (see [Section 3.7](#37-mint-generated)). #### Bridge Account Storage | Slot name | Slot type | Key encoding | Value encoding | Purpose | |-----------|-----------|-------------|----------------|---------| -| `agglayer::bridge::ger_map` | Map | `rpo256::merge(GER_UPPER, GER_LOWER)` | `[1, 0, 0, 0]` if known; `[0, 0, 0, 0]` if absent | Known Global Exit Root set | -| `agglayer::bridge::let_frontier` | Map | `[h, 0, 0, 0]` and `[h, 1, 0, 0]` (for h = 0..31) | Per index h: two keys yield one double-word (2 words = 8 felts, a Keccak-256 digest). Absent keys return zeros. | Local Exit Tree MMR frontier | -| `agglayer::bridge::let_root_lo` | Value | -- | `[root_0, root_1, root_2, root_3]` | LET root low word (Keccak-256 lower 16 bytes) | -| `agglayer::bridge::let_root_hi` | Value | -- | `[root_4, root_5, root_6, root_7]` | LET root high word (Keccak-256 upper 16 bytes) | +| `agglayer::bridge::ger_map` | Map | `poseidon2::merge(GER_LOWER, GER_UPPER)` | `[1, 0, 0, 0]` if known | Known Global Exit Root set | +| `agglayer::bridge::let_frontier` | Map | `[h, 0, 0, 0]` and `[h, 1, 0, 0]` (for h = 0..31) | Per index h: two keys yield one double-word (2 words = 8 felts, a Keccak-256 digest). | Local Exit Tree | +| `agglayer::bridge::let_root_lo` | Value | -- | Lower word of the LET root | LET root low word (Keccak-256 lower 16 bytes) | +| `agglayer::bridge::let_root_hi` | Value | -- | Upper word of the LET root | LET root high word (Keccak-256 upper 16 bytes) | | `agglayer::bridge::let_num_leaves` | Value | -- | `[count, 0, 0, 0]` | Number of leaves appended to the LET | -| `agglayer::bridge::faucet_registry_map` | Map | `[0, 0, faucet_id_suffix, faucet_id_prefix]` | `[1, 0, 0, 0]` if registered; `[0, 0, 0, 0]` if absent | Registered faucet lookup | +| `agglayer::bridge::faucet_registry_map` | Map | `[0, 0, faucet_id_suffix, faucet_id_prefix]` | `[1, 0, 0, 0]` if registered | Registered faucet lookup | +| `agglayer::bridge::token_registry_map` | Map | `Poseidon2::hash_elements(origin_token_addr[5])` | `[0, 0, faucet_id_suffix, faucet_id_prefix]` | Origin token address to faucet ID lookup | +| `agglayer::bridge::claim_nullifiers` | Map | `Poseidon2::hash_elements(leaf_index, source_bridge_network)` | `[1, 0, 0, 0]` if claimed | Prevents double-claiming of bridge-in deposits | +| `agglayer::bridge::cgi_chain_hash_lo` | Value | -- | Lower word of the CGI chain hash | CGI chain hash low word (Keccak-256 lower 16 bytes) | +| `agglayer::bridge::cgi_chain_hash_hi` | Value | -- | Upper word of the CGI chain hash | CGI chain hash high word (Keccak-256 upper 16 bytes) | | `agglayer::bridge::admin_account_id` | Value | -- | `[0, 0, admin_suffix, admin_prefix]` | Bridge admin account ID for CONFIG note authorization | | `agglayer::bridge::ger_manager_account_id` | Value | -- | `[0, 0, mgr_suffix, mgr_prefix]` | GER manager account ID for UPDATE_GER note authorization | @@ -146,31 +164,51 @@ Initial state: all map slots empty, all value slots `[0, 0, 0, 0]` except The faucet account has the `agglayer_faucet` component (`components/faucet.masm`), which is a thin wrapper that re-exports procedures from the `agglayer` library: -- `faucet::claim` +- `faucet::mint_and_send` - `faucet::asset_to_origin_asset` +- `faucet::get_metadata_hash` +- `faucet::get_scale` - `faucet::burn` The underlying library code lives in `asm/agglayer/faucet/mod.masm` with supporting modules in `asm/agglayer/common/`. -#### `agglayer_faucet::claim` +#### `agglayer_faucet::mint_and_send` | | | |-|-| | **Invocation** | `call` | -| **Inputs** | `[PROOF_DATA_KEY, LEAF_DATA_KEY, faucet_mint_amount, pad(7)]` | -| **Outputs** | `[pad(16)]` | -| **Context** | Consuming a `CLAIM` note on the faucet account | -| **Panics** | Invalid proof; bridge ID not set; FPI to bridge fails; faucet distribution fails | +| **Inputs** | `[amount, tag, note_type, RECIPIENT, pad(9)]` | +| **Outputs** | `[note_idx, pad(15)]` | +| **Context** | Consuming a `MINT` note on the faucet account | +| **Panics** | Faucet owner verification fails; minting exceeds supply | -Processes a bridge-in claim: +Re-export of `miden::standards::faucets::network_fungible::mint_and_send`. Mints the +specified amount and creates an output note with the given recipient. Requires the +faucet's owner (the bridge account) to be the creator of this note (the bridge is stored in `Ownable2Step` storage slot as the owner; the faucet's `mint_and_send` executes the current access policy via `exec.policy_manager::execute_mint_policy`). -1. Loads and verifies two advice map entries (proof data, leaf data) into memory. -2. Extracts the destination account ID from the leaf data's destination address (via `eth_address::to_account_id`). -3. Extracts the raw U256 claim amount from the leaf data. -4. FPI to `bridge_in::verify_leaf_bridge` on the bridge account to validate the proof. -5. Verifies `faucet_mint_amount` (passed on the stack from the CLAIM note script) against the U256 amount and scale factor using `asset_conversion::verify_u256_to_native_amount_conversion`. This ensures the amount conversion was performed correctly off-chain, without requiring expensive U256 division inside the VM. -6. Mints the asset via `faucets::distribute` and creates a public P2ID output note for the recipient. The P2ID serial number is derived deterministically from `PROOF_DATA_KEY` (RPO256 hash of the proof data), and the note tag is computed at runtime from the destination account's prefix. +#### `agglayer_faucet::get_metadata_hash` + +| | | +|-|-| +| **Invocation** | `call` | +| **Inputs** | `[pad(16)]` | +| **Outputs** | `[METADATA_HASH_LO, METADATA_HASH_HI, pad(8)]` | +| **Context** | FPI target - called by the bridge during bridge-out | + +Reads the pre-computed metadata hash from the two faucet storage slots +(`metadata_hash_lo`, `metadata_hash_hi`) and returns it as 8 u32 felts. + +#### `agglayer_faucet::get_scale` + +| | | +|-|-| +| **Invocation** | `call` | +| **Inputs** | `[pad(16)]` | +| **Outputs** | `[scale, pad(15)]` | +| **Context** | FPI target - called by the bridge during bridge-in claim amount verification | + +Reads the scale factor from the `conversion_info_2` storage slot and returns it. #### `agglayer_faucet::asset_to_origin_asset` @@ -185,7 +223,8 @@ Processes a bridge-in claim: Converts a Miden-native asset amount to the origin chain's U256 representation: 1. Reads the scale from storage, calls `asset_conversion::scale_native_amount_to_u256`. -2. Returns the origin token address and origin network from storage. +2. Since `scale_native_amount_to_u256` operates on BE bytes, and Keccak expects LE, the procedure calls `reverse_limbs_and_change_byte_endianness` +3. Returns the origin token address and origin network from storage. #### `agglayer_faucet::burn` @@ -206,6 +245,15 @@ This is a re-export of `miden::standards::faucets::basic_fungible::burn`. It bur | Faucet metadata (standard) | Value | `[token_supply, max_supply, decimals, token_symbol]` | Standard `NetworkFungibleFaucet` metadata | | `agglayer::faucet::conversion_info_1` | Value | `[addr_0, addr_1, addr_2, addr_3]` | Origin token address, first 4 u32 limbs | | `agglayer::faucet::conversion_info_2` | Value | `[addr_4, origin_network, scale, 0]` | Origin token address 5th limb, origin network ID, scale exponent | +| `agglayer::faucet::metadata_hash_lo` | Value | Lower word of the metadata hash | Metadata hash low word (4 u32 felts) | +| `agglayer::faucet::metadata_hash_hi` | Value | Upper word of the metadata hash | Metadata hash high word (4 u32 felts) | + +**Companion component storage slots:** The faucet account also includes storage from +companion components required by `network_fungible::mint_and_send`: + +- `Ownable2Step` owner config slot: stores the bridge account ID as owner. +- `OwnerControlled` slots (3): `active_policy_proc_root`, `allowed_policy_proc_roots`, + `policy_authority`. --- @@ -263,8 +311,9 @@ Keccak preimage format directly — the felt value does **not** equal the numeri ### 3.2 CLAIM -**Purpose:** Claim assets, which were deposited on any AggLayer-connected rollup, on Miden. Consumed by -the faucet (TODO (Future): [Re-orient `CLAIM` note flow](https://github.com/0xMiden/protocol/issues/2506) through the bridge account), which mints the asset and sends it to the recipient. +**Purpose:** Claim assets, which were deposited on any AggLayer-connected rollup, on Miden. +Consumed by the bridge account, which validates the proof, looks up the faucet via the +token registry, and creates a MINT note targeting the faucet. **`NoteHeader`** @@ -275,7 +324,7 @@ the faucet (TODO (Future): [Re-orient `CLAIM` note flow](https://github.com/0xMi | `sender` | Any account (not validated) | | `note_type` | `NoteType::Public` | | `tag` | `NoteTag::default()` | -| `attachment` | `NetworkAccountTarget` -- target is the faucet account; execution hint: Always | +| `attachment` | `NetworkAccountTarget` -- target is the bridge account; execution hint: Always | **`NoteDetails`** @@ -313,15 +362,16 @@ The storage is divided into three logical regions: proof data (felts 0-535), lea **Consumption:** -1. Script asserts consuming account matches the target faucet via `NetworkAccountTarget` - attachment (checked before loading storage). +1. Script asserts consuming account matches the target bridge via `NetworkAccountTarget` + attachment. 2. All 569 felts are loaded into memory. -3. The `miden_claim_amount` is read from memory index 568 and placed on the stack. -4. Proof data and leaf data regions are hashed and inserted into the advice map as two - keyed entries (`PROOF_DATA_KEY`, `LEAF_DATA_KEY`). -5. `agglayer_faucet::claim` is called with `[PROOF_DATA_KEY, LEAF_DATA_KEY, miden_claim_amount]` - on the stack. It validates the proof via FPI to the bridge, verifies the native claim - amount conversion, then mints and creates a P2ID output note. +3. Proof data and leaf data regions are hashed with Poseidon2 and inserted into the + advice map as two keyed entries (`PROOF_DATA_KEY`, `LEAF_DATA_KEY`). +4. The `miden_claim_amount` is read from memory. +5. `bridge_in::claim` is called with `[PROOF_DATA_KEY, LEAF_DATA_KEY, miden_claim_amount]` + on the stack. The bridge validates the proof, checks the claim nullifier, looks up the + faucet via the token registry, verifies the amount conversion, then builds a MINT + output note targeting the faucet. ### 3.3 CONFIG_AGG_BRIDGE @@ -348,17 +398,19 @@ The storage is divided into three logical regions: proof data (felts 0-535), lea |-------|-------| | `serial_num` | Random (`rng.draw_word()`) | | `script` | `CONFIG_AGG_BRIDGE.masb` | -| `storage` | 2 felts -- see layout below | +| `storage` | 7 felts -- see layout below | -**Storage layout (2 felts):** +**Storage layout (7 felts):** | Index | Field | Encoding | |-------|-------|----------| -| 0 | `faucet_id_prefix` | Felt (AccountId prefix) | -| 1 | `faucet_id_suffix` | Felt (AccountId suffix) | +| 0-4 | `origin_token_addr` | 5 x u32 felts (20-byte Ethereum address) | +| 5 | `faucet_id_suffix` | Felt (AccountId suffix) | +| 6 | `faucet_id_prefix` | Felt (AccountId prefix) | **Consumption:** Script validates attachment target, loads storage, and calls -`bridge_config::register_faucet` (which asserts sender is bridge admin). +`bridge_config::register_faucet` (which asserts sender is bridge admin and performs +two-step registration into `faucet_registry_map` and `token_registry_map`). ### 3.4 UPDATE_GER @@ -397,7 +449,7 @@ CLAIM notes can be verified against it. **Consumption:** Script validates attachment target, loads storage, and calls `bridge_config::update_ger` (which asserts sender is GER manager), which computes -`rpo256::merge(GER_UPPER, GER_LOWER)` and stores the result in the GER map. +`poseidon2::merge(GER_LOWER, GER_UPPER)` and stores the result in the GER map. ### 3.5 BURN (generated) @@ -422,7 +474,7 @@ CLAIM notes can be verified against it. | Field | Value | |-------|-------| -| `serial_num` | Derived as `rpo256::merge(B2AGG_SERIAL_NUM, ASSET)` | +| `serial_num` | Derived as `poseidon2::merge(B2AGG_SERIAL_NUM, ASSET_KEY)` | | `script` | Standard BURN script (`miden::standards::notes::burn::main`) | | `storage` | None (0 felts) | @@ -438,7 +490,8 @@ decreases the faucet's total token supply by the burned amount. ### 3.6 P2ID (generated) -**Purpose:** Created by `agglayer_faucet::claim` to deliver minted assets to the recipient. +**Purpose:** Created by the faucet (via `mint_and_send`) when consuming a MINT note, to +deliver minted assets to the recipient. **`NoteHeader`** @@ -459,7 +512,7 @@ decreases the faucet's total token supply by the burned amount. | Field | Value | |-------|-------| -| `serial_num` | Derived deterministically from `PROOF_DATA_KEY` (RPO256 hash of the CLAIM proof data) | +| `serial_num` | Derived deterministically from `PROOF_DATA_KEY` (Poseidon2 hash of the CLAIM proof data) | | `script` | Standard P2ID script (`miden::standards::notes::p2id::main`) | | `storage` | 2 felts -- see layout below | @@ -476,6 +529,64 @@ Consuming account must match `target_account_id` from note storage (enforced by script). All note assets are added to the consuming account via `basic_wallet::add_assets_to_account`. +### 3.7 MINT (generated) + +**Purpose:** Created by `bridge_in::claim` on the bridge account. Consumed by the faucet +to mint and distribute assets to the recipient. + +**`NoteHeader`** + +*`NoteMetadata`:* + +| Field | Value | +|-------|-------| +| `sender` | Bridge account | +| `note_type` | `NoteType::Public` | +| `tag` | `NoteTag::default()` | +| `attachment` | `NetworkAccountTarget` -- target is the faucet account; execution hint: Always | + +**`NoteDetails`** + +*`NoteAssets`:* None (empty - the faucet mints the assets on consumption). + +*`NoteRecipient`:* + +| Field | Value | +|-------|-------| +| `serial_num` | Derived from `PROOF_DATA_KEY` (Poseidon2 hash of the CLAIM proof data) | +| `script` | Standard MINT script (`miden::standards::notes::mint::main`) | +| `storage` | 18 felts -- see layout below | + +**Storage layout (18 felts):** + +| Index | Field | Encoding | +|-------|-------|----------| +| 0 | `tag` | Note tag for the P2ID output note (targeting the destination account) | +| 1 | `amount` | Scaled-down Miden token amount to mint | +| 2 | `attachment_kind` | 0 (none - the inner P2ID note has no attachment) | +| 3 | `attachment_scheme` | 0 (none) | +| 4-7 | `attachment` | `[0, 0, 0, 0]` (empty) | +| 8-11 | `p2id_script_root` | Script root of the P2ID note | +| 12-15 | `serial_num` | Serial number for the P2ID note (same as PROOF_DATA_KEY) | +| 16 | `account_id_suffix` | Destination account suffix | +| 17 | `account_id_prefix` | Destination account prefix | + +**Consumption:** + +The standard MINT script for public note creation loads the 18 storage items from the MINT note note storage and calls the faucet's +`mint_and_send` procedure (re-exported from `network_fungible::mint_and_send`). + +Before minting, `mint_and_send` executes the active mint policy via +`policy_manager::execute_mint_policy`. For AggLayer faucets, the active policy is +`owner_controlled::owner_only`, which calls `ownable2step::assert_sender_is_owner`. This +asserts that the MINT note's sender matches the faucet's owner (the bridge account, set +via the `Ownable2Step` companion component at account creation time). This ensures only +the bridge can trigger minting on the faucet. + +After the policy check passes, `mint_and_send` mints the specified amount and creates a +P2ID output note for the recipient using the storage items (script root, serial number, +destination account ID, tag). + --- ## 4. Amount Conversion @@ -689,3 +800,105 @@ the little-endian bytes within each limb in `NoteStorage` and the big-endian-byt The encoding is a bijection over the set of valid `AccountId` values: for every valid `AccountId`, `from_account_id` followed by `to_account_id` (or the MASM equivalent) recovers the original. + +--- + +## 6. Faucet Registry + +The AggLayer bridge connects multiple chains, each with its own native token ecosystem. +When tokens move between chains, they need a representation on the destination chain. +This section describes how tokens are registered for bridging and the role of the +faucet and token registries. + +Terminology: + +- Native token: a token originally issued on a given chain. For example, USDC on Ethereum + is native to Ethereum; a fungible faucet created directly on Miden is native to Miden. +- Non-native (wrapped) token: a representation of a foreign token, created to track + bridged balances. On Miden, each non-native ERC20 is represented by a dedicated + AggLayer faucet. On EVM chains, each non-native Miden token would be represented by a + deployed wrapped ERC20 contract. + +A faucet must be registered in the [Bridge Contract](#21-bridge-account-component) before it can participate in bridging. The +bridge maintains two registry maps: + +- **Faucet registry** (`agglayer::bridge::faucet_registry_map`): maps faucet account IDs + to a registration flag. Used during bridge-out to verify an asset's faucet is authorized + (see `bridge_config::assert_faucet_registered`). +- **Token registry** (`agglayer::bridge::token_registry_map`): maps Poseidon2 hashes of + native token addresses to faucet account IDs. Used during bridge-in to look up the + correct faucet for a given origin token (see + `bridge_config::lookup_faucet_by_token_address`). + +Both registries are populated atomically by `bridge_config::register_faucet` during the [`CONFIG_AGG_BRIDGE`](#33-config_agg_bridge) note consumption. + +### 6.1 Bridging-in: Registering non-native faucets on Miden + +When a new ERC20 token is bridged to Miden for the first time, a corresponding AggLayer +faucet account must be created and registered. The faucet serves as the mint/burn +authority for the wrapped token on Miden. + +The `AggLayerFaucet` struct (Rust, `src/faucet.rs`) captures the faucet-specific +configuration: + +- Token metadata: symbol, decimals, max_supply, token_supply (TODO Missing information about the token name ([#2585](https://github.com/0xMiden/protocol/issues/2585))) +- Origin token address: the ERC20 contract address on the origin chain +- Origin network: the chain ID of the origin chain +- Scale factor: the exponent used to convert between EVM U256 amounts and Field elements on Miden +- Metadata hash: `keccak256(abi.encode(name, symbol, decimals))`. This is precomputed by the bridge admin at faucet creation time and is currently not verified onchain (TODO Verify metadata hash onchain ([#2586](https://github.com/0xMiden/protocol/issues/2586))) + +Registration is performed via [`CONFIG_AGG_BRIDGE`](#33-config_agg_bridge) notes. The bridge +operator creates a `CONFIG_AGG_BRIDGE` note containing the faucet's account ID and the +origin token address, then sends it to the bridge account. On consumption, the note +script calls `bridge_config::register_faucet`, which performs a two-step registration: + +1. Writes a registration flag under the faucet ID key in the `faucet_registry_map`: + `[0, 0, faucet_id_suffix, faucet_id_prefix]` -> `[1, 0, 0, 0]`. +2. Hashes the origin token address using Poseidon2 and writes + the mapping into the `token_registry_map`: + `hash(origin_token_addr)` -> `[0, 0, faucet_id_suffix, faucet_id_prefix]`. + +The token registry enables the bridge to resolve which Miden-side faucet corresponds to a given +origin token address during CLAIM note processing. When the bridge +processes a [`CLAIM`](#32-claim) note, it reads the origin token address from the leaf data and calls +`bridge_config::lookup_faucet_by_token_address` to find the registered faucet. This +lookup hashes the address with Poseidon2 and retrieves the faucet ID from the token +registry map. If the token address is not registered, the `CLAIM` note consumption will fail. + +This means that the bridge admin must register the faucet on the Miden side before the corresponding tokens can be bridged in. + +The bridge admin is a trusted role, and is the sole entity that can register faucets on the Miden side (due to the caller restriction on [`bridge_config::register_faucet`](#bridge_configregister_faucet)). + +### 6.2 Bridging-out: How Miden-native tokens are registered on other chains + +When an asset is bridged out from Miden, [`bridge_out::bridge_out`](#bridge_outbridge_out) constructs a leaf for +the Local Exit Tree. The leaf includes the metadata hash, which the bridge fetches from +the faucet via FPI (`agglayer_faucet::get_metadata_hash`), as well as the other leaf data fields, including origin network and origin token address. + +On the EVM destination chain, when a user claims the bridged asset via +`PolygonZkEVMBridgeV2.claimAsset()`, the wrapped token is deployed lazily on first claim. +The claimer provides the raw metadata bytes (the ABI-encoded name, symbol, and decimals) +as a parameter to `claimAsset()`. The EVM bridge verifies that +`keccak256(metadata_bytes) == metadataHash` from the Merkle leaf. If the hash matches and +no wrapped token exists yet, the bridge deploys a new `TokenWrapped` ERC20 using the +decoded name, symbol, and decimals from the metadata bytes. + +#### Miden-native faucets + +A Miden-native faucet uses the same storage +layout and registration flow as a wrapped faucet. The key difference is what values are +stored in the conversion metadata: + +- `origin_token_address`: the faucet's own `AccountId` as per the [Embedded Format](#52-embedded-format). +- `origin_network`: Miden's network ID as assigned by AggLayer (currently unassigned). +- `metadata_hash`: `keccak256(abi.encode(name, symbol, decimals))` - same as for wrapped + faucets. + +On the EVM side, `claimAsset()` sees `originNetwork != networkID` (foreign asset), so it +follows the wrapped token path: computes +`tokenInfoHash = keccak256(abi.encodePacked(originNetwork, originTokenAddress))`, and +deploys a new `TokenWrapped` ERC20 via `CREATE2` on first claim, minting on subsequent +claims. The `CREATE2` salt is `tokenInfoHash`, so the wrapper address is deterministic +from the `(originNetwork, originTokenAddress)` pair. The metadata bytes provided by the +claimer (which must hash to the leaf's `metadataHash`) are used to initialize the wrapped +token's name, symbol, and decimals. diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index 3333f680c3..6ebe17236a 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -200,7 +200,8 @@ pub proc claim exec.claim_batch_pipe_double_words # => [pad(16)] - exec.get_destination_account_id_data + exec.load_destination_address + exec.eth_address::to_account_id loc_store.CLAIM_DEST_ID_SUFFIX_LOCAL loc_store.CLAIM_DEST_ID_PREFIX_LOCAL # => [pad(16)] @@ -215,7 +216,7 @@ pub proc claim # => [pad(16)] # Look up the faucet account ID from the origin token address - exec.get_origin_token_address + exec.load_origin_token_address # => [origin_token_addr(5), pad(16)] exec.bridge_config::lookup_faucet_by_token_address @@ -458,7 +459,7 @@ proc verify_claim_amount # => [scale] # Step 3: Load the raw U256 amount from leaf data memory - exec.get_raw_claim_amount + exec.load_raw_claim_amount # => [x7, x6, x5, x4, x3, x2, x1, x0, scale] # Step 4: Load faucet_mint_amount (y) and position it for verification @@ -728,28 +729,24 @@ end #! Extracts the destination account ID as address[5] from memory. #! -#! This procedure reads the destination address from the leaf data and converts it from -#! Ethereum address format to AccountId format (suffix, prefix). +#! Reads the destination address (5 felts) from the leaf data in memory. #! #! Inputs: [] -#! Outputs: [suffix, prefix] +#! Outputs: [address[5]] #! #! Invocation: exec -proc get_destination_account_id_data +proc load_destination_address mem_load.DESTINATION_ADDRESS_4 mem_load.DESTINATION_ADDRESS_3 mem_load.DESTINATION_ADDRESS_2 mem_load.DESTINATION_ADDRESS_1 mem_load.DESTINATION_ADDRESS_0 # => [address[5]] - - exec.eth_address::to_account_id - # => [suffix, prefix] end # Inputs: [] # Outputs: [U256[0], U256[1]] -proc get_raw_claim_amount +proc load_raw_claim_amount mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 @@ -766,7 +763,7 @@ end #! Outputs: [origin_token_addr(5)] #! #! Invocation: exec -proc get_origin_token_address +proc load_origin_token_address mem_load.ORIGIN_TOKEN_ADDRESS_4 mem_load.ORIGIN_TOKEN_ADDRESS_3 mem_load.ORIGIN_TOKEN_ADDRESS_2 diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index 1e4755fa3e..c94b00a62b 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -17,7 +17,7 @@ use agglayer::common::utils use agglayer::faucet -> agglayer_faucet use agglayer::bridge::bridge_config use agglayer::bridge::leaf_utils -use agglayer::bridge::mmr_frontier32_keccak +use agglayer::bridge::merkle_tree_frontier use agglayer::common::utils::EthereumAddressFormat # CONSTANTS @@ -90,7 +90,7 @@ const BURN_NOTE_NUM_STORAGE_ITEMS=0 #! 1. Validates the asset's faucet is registered in the bridge's faucet registry #! 2. Queries the faucet for origin asset conversion data via FPI #! 3. Builds the leaf data (origin token, destination, amount, metadata) -#! 4. Computes Keccak hash and adds it to the MMR frontier +#! 4. Computes Keccak hash of the leaf data and appends it to the Local Exit Tree #! 5. Creates a BURN note with the bridged out asset #! #! Inputs: [ASSET_KEY, ASSET_VALUE, dest_network_id, dest_address(5), pad(2)] @@ -206,7 +206,7 @@ pub proc bridge_out mem_store # => [pad(16)] - # --- 4. Compute leaf value and add to MMR frontier --- + # --- 4. Compute leaf value and append it to the Local Exit Tree --- push.LEAF_DATA_START_PTR exec.add_leaf_bridge # => [pad(16)] @@ -279,7 +279,7 @@ proc convert_asset # => [AMOUNT_U256[0](4), AMOUNT_U256[1](4), origin_addr(5), origin_network] end -#! Computes the leaf value from the leaf data in memory and adds it to the MMR frontier. +#! Computes the leaf value from the leaf data in memory and appends it to the Local Exit Tree. #! #! Inputs: [leaf_data_start_ptr] #! Outputs: [] @@ -310,7 +310,7 @@ proc add_leaf_bridge(leaf_data_start_ptr: MemoryAddress) # => [LEAF_VALUE_LO, LEAF_VALUE_HI, let_frontier_ptr] # Append the leaf to the frontier and compute the new root - exec.mmr_frontier32_keccak::append_and_update_frontier + exec.merkle_tree_frontier::append_and_update_frontier # => [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] # Save the root and num_leaves to their value slots diff --git a/crates/miden-agglayer/asm/agglayer/bridge/mmr_frontier32_keccak.masm b/crates/miden-agglayer/asm/agglayer/bridge/merkle_tree_frontier.masm similarity index 67% rename from crates/miden-agglayer/asm/agglayer/bridge/mmr_frontier32_keccak.masm rename to crates/miden-agglayer/asm/agglayer/bridge/merkle_tree_frontier.masm index 32eef7cec1..ed613d1bf9 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/mmr_frontier32_keccak.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/merkle_tree_frontier.masm @@ -3,24 +3,27 @@ use ::agglayer::bridge::canonical_zeros::load_zeros_to_memory use ::agglayer::common::utils::mem_store_double_word use ::agglayer::common::utils::mem_load_double_word -# An MMR Frontier is a data structure based on an MMR, which combines some features of an MMR and an -# SMT. +# A Merkle Tree Frontier (MTF) is a data structure based on an Merkle Mountain Range (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. +# The main entity in this structure is a _frontier_: it could be described either as a set of roots +# of all individual trees in the MMR, or as a Merkle Path to the last added leaf to the append-only, +# fixed-depth, canonical-zero initialized Merkle Tree. +# In the AggLayer code this value is called "branch" (see here: ) +# Let's consider the MMR 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: +# they are sufficient to compute the root of the entire MMR. If we add another node, the MMR will +# become a full binary tree and will look like so: # # 15 # / \ @@ -28,67 +31,74 @@ use ::agglayer::common::utils::mem_load_double_word # / \ # 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. +# An MTF consists of the leaves number currently pushed to the structure and the array containing +# the frontier. +# +# For the sake of simplicity, this frontier array has a fixed length, equal to the maximum height of +# the hypothetical corresponding Merkle Tree. Currently this height is set to 32. # 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. +# To compute the hash of two MTF 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 +# Each node in an MTF 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 MTF 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. +# Each state of the MTF is represented by the root. This root corresponds to (is equal to) a root of +# the fixed-depth, canonical-zero initialized Merkle Tree. For this implementation this depth is set +# to 32. "Canonical-zero initialized" here means that every value of height `h` in this Tree is +# initialized with `value_{h} = Keccak256::hash(value_{h-1}, value_{h-1})`, where `value_0` (a leaf +# value) is a plain 0. # # # Layout # -# The memory layout of the MMR frontier looks like so: +# The memory layout of the MTF looks like so: # -# [num_leaves, 0, 0, 0, [FRONTIER_VALUE_DW]] +# [num_leaves, x, x, x, [FRONTIER_VALUE_DW]] +# +# Notice that the three values after `num_leaves` are never accessed and are only needed to +# word-align the frontier array. Therefore, these three values can be any field element. # # 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. +# - num_leaves is the number of leaves in the MTF before adding the new leaf. +# - [FRONTIER_VALUE_DW] is an array containing the double words which represent the frontier nodes +# of this MTF. 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. +# Canonical 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)" +const ERR_MTF_LEAVES_NUM_EXCEED_LIMIT = "number of leaves in the MTF would exceed 4294967295 (2^32 - 1)" # CONSTANTS # ================================================================================================= -# The maximum number of leaves which could be added to the MMR. +# The maximum number of leaves which could be added to the MTF. # # 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. +# The total height of the full MTF, 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. +# The offset of the number of leaves in the current MTF memory state. const NUM_LEAVES_OFFSET = 0 # The offset of the array of the frontier nodes of respective heights. @@ -109,10 +119,10 @@ 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. +#! Updates the existing frontier with the new leaf, returns a new leaf count and a new MTF root. #! -#! The memory layout at the `mmr_frontier_ptr` is expected to be: -#! [num_leaves, [[FRONTIER_NODE_LO, FRONTIER_NODE_HI]; 32]] +#! The memory layout at the `mtf_ptr` is expected to be: +#! [num_leaves, 0, 0, 0, [[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. #! @@ -121,83 +131,83 @@ const CANONICAL_ZEROES_LOCAL = 8 #! 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] +#! Inputs: [NEW_LEAF_LO, NEW_LEAF_HI, mtf_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. +#! to the MTF. +#! - mtf_ptr is the pointer to the memory where the MTF structure is located. +#! - [NEW_ROOT_LO, NEW_ROOT_HI] is the new root of the MTF, represented as Keccak256 hash. +#! - new_leaf_count is the number of leaves in the MTF after the new leaf was added. #! #! Panics if: -#! - The number of leaves in the MMR has reached the maximum limit of 2^32. +#! - The number of leaves in the MTF 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_le.CUR_HASH_LO_LOCAL dropw loc_storew_le.CUR_HASH_HI_LOCAL dropw - # => [mmr_frontier_ptr] + # => [mtf_ptr] # get the current leaves number dup add.NUM_LEAVES_OFFSET mem_load - # => [num_leaves, mmr_frontier_ptr] + # => [num_leaves, mtf_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 + # make sure that the MTF is not full yet and we still can store the new leaf + # the MTF 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] + u32assert.err=ERR_MTF_LEAVES_NUM_EXCEED_LIMIT + dup u32lte.MAX_LEAVES_MINUS_1 assert.err=ERR_MTF_LEAVES_NUM_EXCEED_LIMIT + # => [num_leaves, mtf_ptr] # get the memory pointer where the canonical zeros will be stored locaddr.CANONICAL_ZEROES_LOCAL - # => [zeros_ptr, num_leaves, mmr_frontier_ptr] + # => [zeros_ptr, num_leaves, mtf_ptr] # load the canonical zeros into the memory exec.load_zeros_to_memory - # => [num_leaves, mmr_frontier_ptr] + # => [num_leaves, mtf_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] + # => [num_leaves_ptr, num_leaves+1, num_leaves, mtf_ptr] mem_store - # => [num_leaves, mmr_frontier_ptr] + # => [num_leaves, mtf_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] + # => [loop_flag=1, iter_counter=0, num_leaves, mtf_ptr] while.true - # => [curr_tree_height, num_leaves, mmr_frontier_ptr] + # => [curr_tree_height, num_leaves, mtf_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] + # => [frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mtf_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 + # is_odd, frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mtf_ptr # ] if.true - # => [frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mmr_frontier_ptr] + # => [frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mtf_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 + # num_leaves, mtf_ptr # ] # load the current hash from the local memory back to the stack @@ -208,83 +218,83 @@ pub proc append_and_update_frontier 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 + # CUR_HASH_HI, curr_tree_height, num_leaves, mtf_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] + # => [CUR_HASH_LO', CUR_HASH_HI', curr_tree_height, num_leaves, mtf_ptr] # store the current hash of the next height back to the local memory loc_storew_le.CUR_HASH_LO_LOCAL dropw loc_storew_le.CUR_HASH_HI_LOCAL dropw - # => [curr_tree_height, num_leaves, mmr_frontier_ptr] + # => [curr_tree_height, num_leaves, mtf_ptr] else - # => [frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mmr_frontier_ptr] + # => [frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mtf_ptr] # - # this height wasn't "occupied" yet: store the current hash as the subtree root - # (frontier node) at height `curr_tree_height` + # this height wasn't "occupied" yet: store the current hash as the frontier node at + # height `curr_tree_height` padw loc_loadw_le.CUR_HASH_HI_LOCAL padw loc_loadw_le.CUR_HASH_LO_LOCAL # => [ # CUR_HASH_LO, CUR_HASH_HI, frontier[curr_tree_height]_ptr, curr_tree_height, - # num_leaves, mmr_frontier_ptr + # num_leaves, mtf_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] + # => [CUR_HASH_LO, CUR_HASH_HI, curr_tree_height, num_leaves, mtf_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 + # mtf_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 + # mtf_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] + # => [CUR_HASH_LO', CUR_HASH_HI', curr_tree_height, num_leaves, mtf_ptr] # store the current hash of the next height back to the local memory loc_storew_le.CUR_HASH_LO_LOCAL dropw loc_storew_le.CUR_HASH_HI_LOCAL dropw - # => [curr_tree_height, num_leaves, mmr_frontier_ptr] + # => [curr_tree_height, num_leaves, mtf_ptr] end - # => [curr_tree_height, num_leaves, mmr_frontier_ptr] + # => [curr_tree_height, num_leaves, mtf_ptr] # update the current tree height add.1 - # => [curr_tree_height+1, num_leaves, mmr_frontier_ptr] + # => [curr_tree_height+1, num_leaves, mtf_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] + # => [curr_tree_height+1, num_leaves>>1, mtf_ptr] # compute the cycle flag dup neq.TREE_HEIGHT - # => [loop_flag, curr_tree_height+1, num_leaves>>1, mmr_frontier_ptr] + # => [loop_flag, curr_tree_height+1, num_leaves>>1, mtf_ptr] end - # => [curr_tree_height=TREE_HEIGHT, num_leaves=0, mmr_frontier_ptr] + # => [curr_tree_height=TREE_HEIGHT, num_leaves=0, mtf_ptr] # clean the stack drop drop - # => [mmr_frontier_ptr] + # => [mtf_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. + # The current (final) hash represents the root of the whole MTF. # # 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 diff --git a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm index 913ebfa6ac..0b9c00db78 100644 --- a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm +++ b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm @@ -34,7 +34,6 @@ const METADATA_HASH_HI_SLOT = word("agglayer::faucet::metadata_hash_hi") #! #! Invocation: exec pub proc get_origin_token_address - # TODO(migration): this procedure name is the same as the one in the bridge account, but they have different functions. push.CONVERSION_INFO_1_SLOT[0..2] exec.active_account::get_item # => [addr0, addr1, addr2, addr3] diff --git a/crates/miden-agglayer/solidity-compat/README.md b/crates/miden-agglayer/solidity-compat/README.md index b45b6edced..9c7f3a2aeb 100644 --- a/crates/miden-agglayer/solidity-compat/README.md +++ b/crates/miden-agglayer/solidity-compat/README.md @@ -1,7 +1,7 @@ # Solidity Compatibility Tests This directory contains Foundry tests for generating test vectors to verify -that the Miden MMR Frontier implementation is compatible with the Solidity +that the Miden Merkle Tree Frontier implementation is compatible with the Solidity `DepositContractBase.sol` from [agglayer-contracts v2](https://github.com/agglayer/agglayer-contracts). ## Prerequisites @@ -15,7 +15,7 @@ foundryup ## Generating Test Vectors -From the repository root, you can regenerate both canonical zeros and MMR frontier test vectors with: +From the repository root, you can regenerate both canonical zeros and Merkle Tree Frontier test vectors with: ```bash make generate-solidity-test-vectors @@ -30,28 +30,28 @@ forge install # Generate canonical zeros (test-vectors/canonical_zeros.json) forge test -vv --match-test test_generateCanonicalZeros -# Generate MMR frontier vectors (test-vectors/mmr_frontier_vectors.json) +# Generate Merkle Tree Frontier vectors (test-vectors/merkle_tree_frontier_vectors.json) forge test -vv --match-test test_generateVectors ``` ## Generated Files - `test-vectors/canonical_zeros.json` - Canonical zeros for each tree height (ZERO_n = keccak256(ZERO_{n-1} || ZERO_{n-1})) -- `test-vectors/mmr_frontier_vectors.json` - Leaf-root pairs after adding leaves 0..31 +- `test-vectors/merkle_tree_frontier_vectors.json` - Leaf-root pairs after adding leaves 0..31 ### Canonical Zeros The canonical zeros should match the constants in: `crates/miden-agglayer/asm/bridge/canonical_zeros.masm` -### MMR Frontier Vectors +### Merkle Tree Frontier Vectors The `test_generateVectors` adds 32 leaves and outputs the root after each addition. Each leaf uses: - `amounts[i] = i + 1` - `destination_networks[i]` and `destination_addresses[i]` generated deterministically from - a fixed seed in `MMRTestVectors.t.sol` + a fixed seed in `MTFTestVectors.t.sol` This gives reproducible "random-looking" destination parameters while keeping vector generation stable across machines and reruns. diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/merkle_tree_frontier_vectors.json b/crates/miden-agglayer/solidity-compat/test-vectors/merkle_tree_frontier_vectors.json new file mode 100644 index 0000000000..d4fec09c98 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test-vectors/merkle_tree_frontier_vectors.json @@ -0,0 +1,210 @@ +{ + "amounts": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32 + ], + "counts": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32 + ], + "destination_addresses": [ + "0x29774ecA2a6fb51515DC9Cf7901a2f177491B465", + "0x89AC74be66304a8Ccd175D2f3fC7467e2c98549E", + "0xfA84Bac9B076cbb787a38F6693E7521c94C4990e", + "0x54e80CF9B0a6905D6a8ac207211DFf095ADf9f58", + "0x54B09CB7E74217Abe17e9F0BB05a735Cb132c14f", + "0x068F93b40701Ef22CF47879dd05169700C02CEE4", + "0x3640b94149e676Ba51f02AA786f5bF4ca2E5C6F2", + "0x6111B111Ca1847BFb463A4C937b57F97B0BFDd36", + "0x17ed25e19F5650532D8B071ED47CFE645143cb21", + "0x1c0e30B1A90fCDcf73fc793a32651D0cBa43De21", + "0x5224B441B7EDdd153b7a70a40BAa1a1433E78595", + "0x49E7dc9ed591B1c9f42Af30e87A531636e2af3F7", + "0xD6C3d1FfABE7a6FE774D939cc7931CE2721d8F8d", + "0xf78166B143624B5C2C888226705404Ec78f2309d", + "0xb557dcb2815a378974fadf1353caE7671Dedb154", + "0x5aCc1E437CC4B04563451B0c0bD69358AB32a036", + "0xe4eb965ea3BCC31Ba12A60AA82814AcfF7929Fc0", + "0xeDde23A9Ed57fD293275Ea1198115eb323a2729E", + "0xa8867c7de34E5660521d423C3866293DF901796B", + "0x4cDD6Db5BaCBa3049Fe81D96006496B7666Ac0f4", + "0xe97C93894f0a0611586d459dE7d38dC3933aD79d", + "0x7F999b8333Aa94036F8d4A9D4eE578797F8a6c4a", + "0xA67c82fAD08bcC9d32B4CbFdfd4FFfbddBd6bDB8", + "0xb29f50353F68A80EF532CE39Ea75F293214229B6", + "0x322da904E659D492D864AFa39413b8C3D6E9f904", + "0xe0CC834e9Af44BA0Eeb71AdfaB33cA95BD3f6A18", + "0xD2B294001FAEBf7a9ffABdD13C0522B0c98f0C1e", + "0x88BCB2126e0AA3C6A540E5D42b446Ac07d97661D", + "0xb7343028f64666414479D70d557fe48529F12174", + "0x2f2c21C34be728DbD122E84056088F3fd9bccCd4", + "0x6d47ceC36d416A20c012DaAD3794F435A5dDA367", + "0x405D4D037420A6719C2bC17c3D1F484ed45f2d0C" + ], + "destination_networks": [ + 696366010, + 1516598451, + 3741447329, + 2423239855, + 4033794240, + 1301407838, + 3643234259, + 4178223902, + 1526316765, + 4162708200, + 1834081884, + 2408708459, + 3987969102, + 2457295469, + 2869269470, + 374449444, + 3352894919, + 1252205322, + 1161212526, + 74657160, + 718169409, + 3846068357, + 1770709979, + 906926958, + 2991336661, + 1320159820, + 2846942876, + 1582740846, + 1398597160, + 4034541345, + 3261712996, + 3294915630 + ], + "leaves": [ + "0x60eb89d7ffbe13411cf68138e7790d71718f576968dcb783e5f698470f876d82", + "0x439be2bd854b64a94abee0d68730ab69c866a4e0439bde12eb79c6a55a1393e7", + "0xf240433f6fae88ba9afdbdb29c406c785311453d9fa16f490216c1a7e8f26a00", + "0xe3f1f3a017c268ab291817e29e07660b94f4d1ff2f0cdc6ae0d50fe749737e54", + "0x10889c0de7e41eb380e143d8429440ce32c5b8b87f39216f66529e9e34e18c55", + "0x3f310ed2fd0fb386991b411b394fead9d6cc70a9995e2b62653d14255a6eaa61", + "0x6df19520edf76c50da67ff52aa6436d5d5ef7cceb4f2224e0ba2163162e741ed", + "0x3e596f414798aabbb9671a2bc9d2e05efcf64ecd0b394d803734003643882110", + "0xc371ea13fe97dad61a2f30cca8117722c1f79a3f6be8badacee4f7747b484595", + "0x5e587bf30a91d9d12fbf49b6467f6a192c3e4f98f9bd6e70d51bcf45e5e36de8", + "0x2e9db615d2960a3404118e91a430c774b2793121eab2c068fd5d86398f18bf98", + "0x9cc92d60479185a9011a96a135a3d14e87d71e2365c68ce8efa70024a7a6a130", + "0xdff712a5cd7d86f75b4c1225ce3c0f619ddf75ab2a795dbdb182971cb30fbce8", + "0x0b037c509dd86651dfa5f81bd1dd9d7f26717a78e1f483fceed201dbcc3e4492", + "0xf1fc1cb988190664be4769736de57ffc4873dd4023878d0091b1f2b9cd77cc24", + "0x0aaa9f1fb2d7594923640ef0b3b113e557b2530bb0c07f889e0112a440bbb80b", + "0x3c90d9bedc2870216fc7f1416a901483f9093eaf1b9a7944c05861f7ebdbdfda", + "0x3a33918becdb02da4e445b869015a7d5410d6e42a7efd2f3abaec2320b62a5dc", + "0xda1e66211dd699eaae4b5768aa9f4264a53167e946bdcd330bff9b0955eb7342", + "0xbb310dd3512875f8588553e41623a687c618c5def86d86bfbad305fb1a4b7869", + "0x5257ed10726840a181072b383ad65cc934b7e835f385a0b8742822df9eaa97d2", + "0xad950a4d6e9dd188ff4ca1783a7934a4f23abfb9ce6e211b07dd4d70702ae771", + "0x1901aab4548d4ccf0a107cb5a6d7edf5d10dab146cd7248d0e94b4a73323b62e", + "0xf819e474ea878d2fd00c7a1a1a108123dcccdb5de030d8baab831174b7e66335", + "0xdaa7025d37eb7ff665a7ece567c596adc64ad424630a999955e5752e92563682", + "0xc88d2e559fa70a96782cfff7284f8e84a4f559140e9394fa69ef2195843485eb", + "0x7515e9c34b274f720fb10210e69a3cb326197192b31084557bfd6279a54cf6ba", + "0x7c3e73e589400c6857a93dca10008e81c975275323bd454bb0b18839821715f5", + "0x03eb7634fd1dc47dc2625bd7918e1afe2312925e965d45d93dab7542fc43ee0d", + "0x34cbc7af8981f24827464c8355b1c2c175e714a17ba6b4b220fe81d79c8ad6f9", + "0xe0ba1ef6b0caccac39aa99d22414a8abe4c7820a4cbae2a55a8dbafdf8251ed5", + "0xb5b2e50af030c0aa4d3f9ff79bf275d8cbd46199c94642070a8061ab02656a7e" + ], + "origin_token_address": "0x7a6fC3e8b57c6D1924F1A9d0E2b3c4D5e6F70891", + "roots": [ + "0x1f1975e4644389a022c75f56f41790ebea9fc8b6a7fcc6cc97c4d55aaeff7a72", + "0xa38a8ea19c384c1a68d6f2d5114c59342c331bc613c674535216d7c4f3d59e3c", + "0xda68c6dc7d5a4530a33abdf93fc411cc83711479d46917405f49a33eab7a0fb6", + "0x3e12f69a082121f76ecc90135beedf30fab1294f2fd14fcc560f1ec39283660c", + "0xa0c4361fef22ff84456ef63d567aa5a5e4a03c67f525402d20b8d21bc526e0cf", + "0x99dd12cbcbb4e67fd707ed391ffae4576c2eb5ff46e23b3a52552842e77498c9", + "0x0312a3f12aff3458fa1b8f5a8af421e3efb653d760dd2c86d2f051d2443d7ee3", + "0xc9b9d68f6340ea3a09dfd9b474bbd52dc73edfb412e40fde6b387e89d03bdc44", + "0x48fd4f8a05527e969a038f2c41ba1cdbc5b9d1690f4ac80781e249e6951aa669", + "0x65c8d14e7ee36a61f5fa7c03e4f513a95be5fcaec7ddb6b5e7986d240c4092e3", + "0x15e9b565eec210f02668dcbf30333ae13c1619da9bf4776895a8a3b3853d3ba8", + "0xf2ded207cabbddfadb19e96ad5faae5a1d8ba18916be9a280df2ba4cd26cd89b", + "0xdc3fdb7d81600feacf043579ad9998f2b071c842c66afa788697a7c41282bd4a", + "0x139490c416b046c5d29c0b74abe4ddba77a070d0cd062934c4494fc0601bbcd6", + "0x8ef8958be5d3a37cbe4c5cb23e8ac5fc2b73a8f4627f6b412765e732e09d5723", + "0xeef6287825702d1d752da8d0e7cf7d826fabbb8f55c28f5ebac84e801734fb07", + "0x888eb4695535b6b1ed3896792d60fd9c84b72cf9ad147cbdd82c18cf427ffd01", + "0xd3f52190ddc136a295cc0012776be0470e6b7c5c8f2306181edb9b2248e7d76d", + "0x8e537ee72d70cd168e8ff19f294d8486f79c0eb4bb1da0e7c135e7cf8737f1d5", + "0x9afade513c4333e3d43a009e8945116e0b370282d8914b323b0e0cebe4b4d2fd", + "0xc4b416c7216703e9be07bdc94b68a5c4b6a8e8ab1f3f28e3043c4759a2a3eab2", + "0xada6050ec2ef7a5816b7ec46b2ca49f644f1f66c3375899573059144659118fb", + "0x6d4f6462e07f01fdab8be572fae91f2a030e328fbc6d39f96abc767011e5e94f", + "0xd54de143d12709d5622f510b4392ef5e187ad1466349ad8205b64fa28e382688", + "0x6aecef7ea1ca99a74f036174f57038967478fd772d05efd056d11957d0080209", + "0x360bb12c37d37626fb97845a0ec1308da3b20721d62bb5679115e83a4ef0aace", + "0xce85a347d8f4b629c7ca41c26b13cd49301dd9fac5c8518a544f04dc20731fd8", + "0xe4d10fbf48da63804882e2222ed3e027338c7a525df9386d8b8309a60994fb27", + "0xb1951105c31c95812290dd388b5d5858831996e67ec9cc84984dcc23c34e59f6", + "0x6925efc941294c66efffc866b003416ccb85dcda1c6c0a69abbe724972912c79", + "0xc87eb0c625327bf0b87cbf61ac3d2602a6ef23f6311ec0eaf3ffd69ad31708c0", + "0xd72c15f2ba3d136e1a5625ce29aec27c5208727f75e980ba37f2a1887e7fdd71" + ], + "token_decimals": 8, + "token_name": "AGG", + "token_symbol": "AGG" +} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json b/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json deleted file mode 100644 index df788f9498..0000000000 --- a/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json +++ /dev/null @@ -1,210 +0,0 @@ -{ - "amounts": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32 - ], - "counts": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32 - ], - "destination_addresses": [ - "0xB48074703337bEf6e94A9e2E1FfFe71632B42D56", - "0xBA60cd3cBD12619e6983B5D0E1CbcF2f4fed9d7b", - "0x89510362d6EdeB958F059727C9eD0F99298aAFa4", - "0xD62Cf6356E0a48e2014b71Cf942BEbBbFb00F7d7", - "0xFA5eacb9668731D74F2BB5Ad5bfB319f5A91c87D", - "0x90DD6647e5c91f9104a548876868a54795696B34", - "0x0E76F5f993A9a7f961e06397BC71d15c278A0b6c", - "0xe022226D1fFcCf12ac0e84D0aB9430F3fd56C613", - "0x1F9ecff77E28Bca8Ef18434B842A30579Bfd4EaA", - "0xe51D207B549Db157BeE9faeBd51C35aB47d180EF", - "0x9f30d6d0335E91e0593f13a567E4Fee661e1259F", - "0xE8F13Da1BDb719ba364a890a623454040A932eCf", - "0xb6EE19bf265563aA76dbe202e8dC71F8f42a58B1", - "0xf62d45e4D0DC57259B4557b5d79Ea23F67D0E381", - "0xaa94f5480aD0C906044E5E7Da8BB6BC4395aA498", - "0x060ddd9f6e6CF285004e33C30b46710ad75918Dd", - "0x8B743c166e1dA1444781AD2b5Fe2291578ABCeb1", - "0x8B08d9A773273Df976fb7448D38FeEeB15Dc34F8", - "0xbe931f6F189e6F8Da14f7B67Eb2E67b5D7f71c1d", - "0x2F891C182b23d1422D8Fddd9CC30B25BB849Bd5F", - "0x93fD7DEd75058ABA1B76C35c4Ac4e9355e596EdC", - "0x25B9eBC8D7d48a6B0e71e82Aa66832aCC9419E3A", - "0xbb086ECaC1316B81107e3CA591ef645831094E5a", - "0x08c7a5Db749DEf9280108Ec5e0354d4957CB17cF", - "0x0da76aA44116fad143F778f25907046E52F8c4d3", - "0xcFd0a3bfA35E771aad88C64EF0A310efF6730cDa", - "0xa7439b51638F31f054C93EC869C8c7E982699BAC", - "0x5C9A97f096CB18903994C44ddC07FfD921490B2c", - "0x0e52786aF0b48D764a255f6506C9C297d5BA2Dc3", - "0x5C2093921171F2c2d657eAA681D463Fe36c965d1", - "0xf8de801F1ba2a676d96Eb1F1ccB0B0CADFCbbE9e", - "0x31D230FAbAd05777Bb3E1a062e781446Bc422b80" - ], - "destination_networks": [ - 1538671592, - 1271685039, - 2812858243, - 1717044446, - 1618236512, - 1846799397, - 1114625417, - 1980472020, - 3445581035, - 1216050355, - 1334555263, - 1595653741, - 1406956437, - 2339872987, - 1591634953, - 2036330440, - 948554316, - 1629580568, - 4209912969, - 3528172732, - 4197496357, - 2020389543, - 1365501531, - 2591126838, - 273689805, - 543018504, - 3291055054, - 2685286074, - 3030491074, - 4166649488, - 1541470110, - 1181416010 - ], - "leaves": [ - "0x583cc77fc2b7280dae7433767e49a7c6d9a33f0410e179814f3aa1dbed9e5383", - "0x39b63728fe06dbc9e883852005cce44a1e6515ed55e8b1dbf3b6758179716f11", - "0xc4971cb8c3f11aedc287a9855739bb007822038c054cb6808e03131c9f91a0f1", - "0x629eb6a1e17d6aea8011061da05909d2f7312467aa8f32738861fb940b157174", - "0xf405ea66eca447509b4ffc555fb9dbcae535b11e55a4331d02819e0ca9984575", - "0xfaa2e8faa0081b6534e90f6fe58e9c5232afe98bcc9f1695e544e02f3569463d", - "0xb89ca15ba6ca7c7a208e24d7353ad31282ef134662f659fac32f27df2ae3320f", - "0x79bdf5742cc5cc4ef8f888a231e367c50b4430a9459541facb343111c92e6bf8", - "0x0822a3dc7f0c51e70dd73014e18df2981c4bde688eec541581558c3de0ad6f65", - "0xbd91e0fa090c5a988b4af55366454b0e66f565f313127d4775bb44e446baa917", - "0xfd3fe60322ffefecdd2e5b9b1ccc99f335dcb63c48bdd4c0694978ff64554abb", - "0xb8f5374c52ea2b64d00f566b798a42fabe4405817327b361cd2e57b17949917a", - "0xd84e6a9f537e1e71ef75ee1b4c9aecaa4f192b65fd3b2c5a276aa82193196c00", - "0xaa746c560d044f6c4ddab4a0553cde8fe6aa95478fe198bcb4b0b9ad3c3b92af", - "0xf9bf642edac2a5f80a899ab3a91aefb6d9afbaf107fa34557c9dc66c6bed4611", - "0xf3b649080b6f226a027260cef334003468ddd40f70f8268b8019613f30f31429", - "0x2e8c2d56396ac75cf085c44ad3939b83f15b3ef886092faaf26373f9083fc49b", - "0x74c483d10393a141f7d1a6d583c324e7b8293f4d8bfa612cfff0a51a8dcd1ddc", - "0x16693082ba7d19cf38216153780011320b4d22133bb541006542f8b24c0bac29", - "0x7132c9fbb1f7ee387c6cdf1ef1554eaa4b791f0de1c2e858a640f3c0e867b1be", - "0x7b0f681fc08c9193034a590e818206c8972887710115677df57113e9b40823cb", - "0xf9513376461d437192b658deaa647a8625e7354f4d59a778114552feaa8b2e70", - "0x4439d51fd28dc9016bbab806805aa36a53fb9a4f02c379b47656d2b4c45c7b39", - "0x3a44b8129f9468dc743ffd55d2cc0390ed565ebaf8955e38a4e8d41714f874c3", - "0xacfc9d4916104a4d0965d1caa24cdf31fa2cf65474f1986175f49ced505d7470", - "0x34a969176d30df0525c1eb1b349d3a24b1a684f5a6f4cf60797d5d213b7007a0", - "0xe16eb94a82b246fa6534867df6ee6217c8b1c850d835d72548d8f85d1504330d", - "0x188e9c5333cc6d9cd5f8c21a71917e22044b1dc6cdc3241ac9187ccd25598884", - "0x2ca6faff026b921ce865e1161688e7debc733c2d699937ce858783ffbca666d8", - "0x0c2819f9ad1daa7dc7a42c0b8c682091dd77c9aa78bbde349b40efd152843b2c", - "0x53c565760b2e54abcf98f888b83a1178a20f47db78aa048738217c0d3e59937b", - "0xc2668ecfa5198b70c0389bc5b71a70f1e2ffe0be832846e1889ad80ee3a8ef34" - ], - "origin_token_address": "0x7a6fC3e8b57c6D1924F1A9d0E2b3c4D5e6F70891", - "roots": [ - "0x8943af888cfe3ea3924601d71a6baacd7b87c826da39e9a682eb285ff5031c1b", - "0x805f8bbc68e3df18db265cfe1fa972faf9e29521978640b34450a8ea8bf7a665", - "0xa0da7520543874392c8332ea2d567ebb4b2b10f6897d34f5404263f0f97b1cb8", - "0x15fd7076633c5dc177f675b9bd39418043177d5cf565f3521a25c502c794f102", - "0x5f3b6ebb3858d5481a1ab0ebc7bf51e66d6dcdaafd861b9e6af088963a5a2282", - "0xce80b3372dd297c3d9cbdce7e0a3b7cf39dabfa03665dc1a2778955935fc06da", - "0xaca8bafe682b461769752c13e7313aa65b83ad1a0019f644a5bdf5e453e1274b", - "0x5453a77465da8cf9803ee6e1ff5960cf96ebd14a2e0cd4299995334ed73e802f", - "0x6bb045a5956579c11c97a673f87292085ba3addda57dcd8c40fbd4db63d8b07f", - "0x4bb0337c1f708b56efc1f0f4279d9ca9c94de2187c406c1619947158d83028ea", - "0x12fac4e9109f79710654946bd345128f8202e403a4ba3fed44efc8d5d0e1cd9e", - "0x2938ec7bc586f16a6cca58e1c3a4b060d135d954649a2abdec9094ebf212fadc", - "0x4d287ab8ab3e87b07528774b18fdb81511402f42d4482bdea43c0a1cbe161128", - "0xa801b801f5018d5e39d7c5aba92e4a4fce13845bab8bf51f198c8165aa20f67d", - "0x343379b96ae9187d4cb8f20706cf4e884f517ef02e3700a9f7500e32f3c14fda", - "0xb18e90e56bbca6ee8a7e653eafef633c01d4e41ef9fdbccfb99c50a5c3ae8f8f", - "0xe2d7f15c9ca938f88337cbcc534475eb6d625cce8fbd24f2389d4874cee21962", - "0x6bb8d1fc420da55e1f42becd9ddd6be0a2b184d7827a410e68314f50bdfc55d5", - "0x4379c4b7761be8fb99d8c33f075d6a8206a15017e7e9a9b41f66eefbac85e99e", - "0x225f99e77a68aad477ae85289bfcf54c86845ab6f5dcb0eea9cb97137a9de128", - "0xcb18011fca44a052414a2f86eb19c63a986868f7cef55f9d1f936e0fd8a1e18a", - "0xbac2a9dbeeeb688616c998fe977f0db04d6021b90fa4e7f0aa1347e8ae8eccef", - "0x94c4018c9810210df4a63db14f1949f6599da6f3c1760efcd4402388a8d9c3b6", - "0x21de8642d818c1ddb0d5f9b5c06a73c1db6c03a753828192a151c08a5e524c80", - "0xd1845fc44e07f7751ab65b05782f1179b5a9212a0f8e980e0e07b56da7663351", - "0xf861aa5ac7127d103e3174753736f3e3110f1317bc1f1c93d638b429ce8a3c9b", - "0xff8c1364e2ff988dbd8780352eeef599341bb010eb48af3019f8540b2b52b90e", - "0xf232b957fa2c9e83120050f8c4324247e12dfbd8f876880383a066f19d018ec6", - "0xd223658da6e25f5362c1abb49484414e4b9594ac7515e0b7d8aabd919866598c", - "0xcf038032ed455f73f04d503cf5796b196dea55d967ec6617f0a1a1623f144ebd", - "0xfa9da8a43eff2ac92f2c3996b2f5b18a92b95a56e61f26bb30ed47122cfc9e9f", - "0x1a17ad0ab073918397c17419deab441d666c0abdeb9f2104c47af4589dd4a2c3" - ], - "token_decimals": 8, - "token_name": "AGG", - "token_symbol": "AGG" -} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/MTFTestVectors.t.sol similarity index 87% rename from crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol rename to crates/miden-agglayer/solidity-compat/test/MTFTestVectors.t.sol index 977dddbc89..c51f13412b 100644 --- a/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/MTFTestVectors.t.sol @@ -5,21 +5,22 @@ import "forge-std/Test.sol"; import "@agglayer/v2/lib/DepositContractV2.sol"; /** - * @title MMRTestVectors + * @title MTFTestVectors * @notice Test contract that generates test vectors for verifying compatibility - * between Solidity's DepositContractBase and Miden's MMR Frontier implementation. + * between Solidity's DepositContractBase and Miden's Merkle Tree Frontier (MTF) + * implementation. * * Leaves are constructed via getLeafValue using the same hardcoded fields that * bridge_out.masm uses (leafType=0, originNetwork=64, originTokenAddress=fixed random value, * metadataHash=0), parametrised by amount (i+1) and deterministic per-leaf * destination network/address values derived from a fixed seed. * - * Run with: forge test -vv --match-contract MMRTestVectors + * Run with: forge test -vv --match-contract MTFTestVectors * - * The output can be compared against the Rust KeccakMmrFrontier32 implementation - * in crates/miden-testing/tests/agglayer/mmr_frontier.rs + * The output can be compared against the Rust MerkleTreeFrontier32 implementation + * in crates/miden-testing/tests/agglayer/merkle_tree_frontier.rs */ -contract MMRTestVectors is Test, DepositContractV2 { +contract MTFTestVectors is Test, DepositContractV2 { // Constants matching bridge_out.masm hardcoded values uint8 constant LEAF_TYPE = 0; uint32 constant ORIGIN_NETWORK = 64; @@ -34,7 +35,7 @@ contract MMRTestVectors is Test, DepositContractV2 { // Fixed seed for deterministic "random" destination vectors. // Keeping this constant ensures everyone regenerates the exact same JSON vectors. - uint256 constant VECTOR_SEED = uint256(keccak256("agglayer::mmr_frontier_vectors::v2")); + uint256 constant VECTOR_SEED = uint256(keccak256("agglayer::merkle_tree_frontier_vectors::v2")); /** * @notice Builds a leaf hash identical to what bridge_out.masm would produce for the @@ -84,7 +85,7 @@ contract MMRTestVectors is Test, DepositContractV2 { } /** - * @notice Generates MMR frontier vectors (leaf-root pairs) and saves to JSON file. + * @notice Generates MTF vectors (leaf-root pairs) and saves to JSON file. * Each leaf is created via _createLeaf(i+1, network[i], address[i]) so that: * - amounts are 1..32 * - destination networks/addresses are deterministic per index from VECTOR_SEED @@ -92,7 +93,7 @@ contract MMRTestVectors is Test, DepositContractV2 { * The destination vectors are also written to JSON so the Rust bridge_out test * can construct matching B2AGG notes. * - * Output file: test-vectors/mmr_frontier_vectors.json + * Output file: test-vectors/merkle_tree_frontier_vectors.json */ function test_generateVectors() public { bytes32[] memory leaves = new bytes32[](32); @@ -131,8 +132,8 @@ contract MMRTestVectors is Test, DepositContractV2 { string memory json = vm.serializeAddress(obj, "destination_addresses", destinationAddresses); // Save to file - string memory outputPath = "test-vectors/mmr_frontier_vectors.json"; + string memory outputPath = "test-vectors/merkle_tree_frontier_vectors.json"; vm.writeJson(json, outputPath); - console.log("Saved MMR frontier vectors to:", outputPath); + console.log("Saved MTF vectors to:", outputPath); } } diff --git a/crates/miden-agglayer/src/b2agg_note.rs b/crates/miden-agglayer/src/b2agg_note.rs index 336fab0491..5309449d93 100644 --- a/crates/miden-agglayer/src/b2agg_note.rs +++ b/crates/miden-agglayer/src/b2agg_note.rs @@ -25,7 +25,7 @@ use miden_protocol::note::{ use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; use miden_utils_sync::LazyLock; -use crate::EthAddressFormat; +use crate::EthAddress; // NOTE SCRIPT // ================================================================================================ @@ -88,7 +88,7 @@ impl B2AggNote { /// Returns an error if note creation fails. pub fn create( destination_network: u32, - destination_address: EthAddressFormat, + destination_address: EthAddress, assets: NoteAssets, target_account_id: AccountId, sender_account_id: AccountId, @@ -120,7 +120,7 @@ impl B2AggNote { /// - 5 felts: destination_address (20 bytes as 5 u32 values) fn build_note_storage( destination_network: u32, - destination_address: EthAddressFormat, + destination_address: EthAddress, ) -> Result { let mut elements = Vec::with_capacity(6); diff --git a/crates/miden-agglayer/src/bridge.rs b/crates/miden-agglayer/src/bridge.rs index c507fffa06..08b493f947 100644 --- a/crates/miden-agglayer/src/bridge.rs +++ b/crates/miden-agglayer/src/bridge.rs @@ -19,14 +19,15 @@ use miden_utils_sync::LazyLock; use thiserror::Error; use super::agglayer_bridge_component_library; -use crate::claim_note::Keccak256Output; +use crate::claim_note::CgiChainHash; pub use crate::{ B2AggNote, ClaimNoteStorage, ConfigAggBridgeNote, - EthAddressFormat, + EthAddress, EthAmount, EthAmountError, + EthEmbeddedAccountId, ExitRoot, GlobalIndex, GlobalIndexError, @@ -309,9 +310,7 @@ impl AggLayerBridge { /// /// Returns an error if: /// - the provided account is not an [`AggLayerBridge`] account. - pub fn cgi_chain_hash( - bridge_account: &Account, - ) -> Result { + pub fn cgi_chain_hash(bridge_account: &Account) -> Result { // check that the provided account is a bridge account Self::assert_bridge_account(bridge_account)?; @@ -333,7 +332,7 @@ impl AggLayerBridge { }) .collect::>(); - Ok(Keccak256Output::new( + Ok(CgiChainHash::new( cgi_chain_hash_bytes .try_into() .expect("keccak hash should consist of exactly 32 bytes"), diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index 1cd3d8dc53..5cd5b9f17f 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -2,7 +2,6 @@ use alloc::string::ToString; use alloc::vec; use alloc::vec::Vec; -use miden_core::utils::bytes_to_packed_u32_elements; use miden_core::{Felt, Word}; use miden_protocol::account::AccountId; use miden_protocol::crypto::SequentialCommit; @@ -11,57 +10,24 @@ use miden_protocol::errors::NoteError; use miden_protocol::note::{Note, NoteAssets, NoteMetadata, NoteRecipient, NoteStorage, NoteType}; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; -use crate::{EthAddressFormat, EthAmount, GlobalIndex, MetadataHash, claim_script}; +use crate::utils::Keccak256Output; +use crate::{EthAddress, EthAmount, GlobalIndex, MetadataHash, claim_script}; -// CLAIM NOTE STRUCTURES +// CLAIM NOTE TYPE ALIASES // ================================================================================================ -/// Keccak256 output representation (32-byte hash) -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Keccak256Output([u8; 32]); - -impl Keccak256Output { - /// Creates a new Keccak256 output from a 32-byte array - pub fn new(bytes: [u8; 32]) -> Self { - Self(bytes) - } - - /// Returns the inner 32-byte array - pub fn as_bytes(&self) -> &[u8; 32] { - &self.0 - } - - /// Converts the Keccak256 output to 8 Felt elements (32-byte value as 8 u32 values in - /// little-endian) - pub fn to_elements(&self) -> Vec { - bytes_to_packed_u32_elements(&self.0) - } - - /// Converts the Keccak256 output to two [`Word`]s: `[lo, hi]`. - /// - /// - `lo` contains the first 4 u32-packed felts (bytes 0..16). - /// - `hi` contains the last 4 u32-packed felts (bytes 16..32). - #[cfg(any(test, feature = "testing"))] - pub fn to_words(&self) -> [Word; 2] { - let elements = self.to_elements(); - let lo: [Felt; 4] = elements[0..4].try_into().expect("to_elements returns 8 felts"); - let hi: [Felt; 4] = elements[4..8].try_into().expect("to_elements returns 8 felts"); - [Word::new(lo), Word::new(hi)] - } -} - -impl From<[u8; 32]> for Keccak256Output { - fn from(bytes: [u8; 32]) -> Self { - Self::new(bytes) - } -} - /// SMT node representation (32-byte Keccak256 hash) pub type SmtNode = Keccak256Output; /// Exit root representation (32-byte Keccak256 hash) pub type ExitRoot = Keccak256Output; +/// Leaf value representation (32-byte Keccak256 hash) +pub type LeafValue = Keccak256Output; + +/// Claimed Global Index (CGI) chain hash representation (32-byte Keccak256 hash) +pub type CgiChainHash = Keccak256Output; + /// Proof data for CLAIM note creation. /// Contains SMT proofs and root hashes using typed representations. #[derive(Clone)] @@ -112,11 +78,11 @@ pub struct LeafData { /// Origin network identifier (uint32) pub origin_network: u32, /// Origin token address - pub origin_token_address: EthAddressFormat, + pub origin_token_address: EthAddress, /// Destination network identifier (uint32) pub destination_network: u32, /// Destination address - pub destination_address: EthAddressFormat, + pub destination_address: EthAddress, /// Amount of tokens (uint256) pub amount: EthAmount, /// Metadata hash (32 bytes) diff --git a/crates/miden-agglayer/src/config_note.rs b/crates/miden-agglayer/src/config_note.rs index c5b2105268..efdd9f6663 100644 --- a/crates/miden-agglayer/src/config_note.rs +++ b/crates/miden-agglayer/src/config_note.rs @@ -28,7 +28,7 @@ use miden_protocol::vm::Program; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; use miden_utils_sync::LazyLock; -use crate::EthAddressFormat; +use crate::EthAddress; // NOTE SCRIPT // ================================================================================================ @@ -93,7 +93,7 @@ impl ConfigAggBridgeNote { /// Returns an error if note creation fails. pub fn create( faucet_account_id: AccountId, - origin_token_address: &EthAddressFormat, + origin_token_address: &EthAddress, sender_account_id: AccountId, target_account_id: AccountId, rng: &mut R, diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index 6ccc7d6f57..045c3226d6 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -46,12 +46,12 @@ pub const ERR_LEADING_BITS_NON_ZERO: MasmError = MasmError::from_static_str("lea /// Error Message: "mainnet flag must be 0 or 1" pub const ERR_MAINNET_FLAG_INVALID: MasmError = MasmError::from_static_str("mainnet flag must be 0 or 1"); -/// 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: "most-significant 4 bytes must be zero for AccountId" pub const ERR_MSB_NONZERO: MasmError = MasmError::from_static_str("most-significant 4 bytes must be zero for AccountId"); +/// Error Message: "number of leaves in the MTF would exceed 4294967295 (2^32 - 1)" +pub const ERR_MTF_LEAVES_NUM_EXCEED_LIMIT: MasmError = MasmError::from_static_str("number of leaves in the MTF 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-agglayer/src/eth_types/address.rs b/crates/miden-agglayer/src/eth_types/address.rs deleted file mode 100644 index 8b489badf8..0000000000 --- a/crates/miden-agglayer/src/eth_types/address.rs +++ /dev/null @@ -1,236 +0,0 @@ -use alloc::format; -use alloc::string::{String, ToString}; -use alloc::vec::Vec; -use core::fmt; - -use miden_core::utils::bytes_to_packed_u32_elements; -use miden_protocol::Felt; -use miden_protocol::account::AccountId; -use miden_protocol::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes}; - -// ================================================================================================ -// ETHEREUM ADDRESS -// ================================================================================================ - -/// Represents an Ethereum address format (20 bytes). -/// -/// # Representations used in this module -/// -/// - Raw bytes: `[u8; 20]` in the conventional Ethereum big-endian byte order (`bytes[0]` is the -/// most-significant byte). -/// - MASM "address\[5\]" limbs: 5 x u32 limbs in *big-endian limb order* (each limb encodes its 4 -/// bytes in little-endian order so felts map to keccak bytes directly): -/// - `address[0]` = bytes[0..4] (most-significant 4 bytes, zero for embedded AccountId) -/// - `address[1]` = bytes[4..8] -/// - `address[2]` = bytes[8..12] -/// - `address[3]` = bytes[12..16] -/// - `address[4]` = bytes[16..20] (least-significant 4 bytes) -/// - Embedded AccountId format: `0x00000000 || prefix(8) || suffix(8)`, where: -/// - prefix = bytes[4..12] as a big-endian u64 -/// - suffix = bytes[12..20] as a big-endian u64 -/// -/// Note: prefix/suffix are *conceptual* 64-bit words; when converting to [`Felt`], we must ensure -/// `Felt::new(u64)` does not reduce mod p (checked explicitly in `to_account_id`). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct EthAddressFormat([u8; 20]); - -impl EthAddressFormat { - // EXTERNAL API - For integrators (Gateway, claim managers, etc.) - // -------------------------------------------------------------------------------------------- - - /// Creates a new [`EthAddressFormat`] from a 20-byte array. - pub const fn new(bytes: [u8; 20]) -> Self { - Self(bytes) - } - - /// Creates an [`EthAddressFormat`] from a hex string (with or without "0x" prefix). - /// - /// # Errors - /// - /// Returns an error if the hex string is invalid or the hex part is not exactly 40 characters. - pub fn from_hex(hex_str: &str) -> Result { - let hex_part = hex_str.strip_prefix("0x").unwrap_or(hex_str); - if hex_part.len() != 40 { - return Err(AddressConversionError::InvalidHexLength); - } - - let prefixed_hex = if hex_str.starts_with("0x") { - hex_str.to_string() - } else { - format!("0x{}", hex_str) - }; - - let bytes: [u8; 20] = hex_to_bytes(&prefixed_hex)?; - Ok(Self(bytes)) - } - - /// Creates an [`EthAddressFormat`] from an [`AccountId`]. - /// - /// **External API**: This function is used by integrators (Gateway, claim managers) to convert - /// Miden AccountIds into the Ethereum address format for constructing CLAIM notes or - /// interfacing when calling the Agglayer Bridge function bridgeAsset(). - /// - /// This conversion is infallible: an [`AccountId`] is two felts, and `as_int()` yields `u64` - /// words which we embed as `0x00000000 || prefix(8) || suffix(8)` (big-endian words). - /// - /// # Example - /// ```ignore - /// let destination_address = EthAddressFormat::from_account_id(destination_account_id).into_bytes(); - /// // then construct the CLAIM note with destination_address... - /// ``` - pub fn from_account_id(account_id: AccountId) -> Self { - let felts: [Felt; 2] = account_id.into(); - - let mut out = [0u8; 20]; - out[4..12].copy_from_slice(&felts[0].as_canonical_u64().to_be_bytes()); - out[12..20].copy_from_slice(&felts[1].as_canonical_u64().to_be_bytes()); - - Self(out) - } - - /// Returns the raw 20-byte array. - pub const fn as_bytes(&self) -> &[u8; 20] { - &self.0 - } - - /// Converts the address into a 20-byte array. - pub const fn into_bytes(self) -> [u8; 20] { - self.0 - } - - /// Converts the Ethereum address to a hex string (lowercase, 0x-prefixed). - pub fn to_hex(&self) -> String { - bytes_to_hex_string(self.0) - } - - // INTERNAL API - For CLAIM note processing - // -------------------------------------------------------------------------------------------- - - /// Converts the Ethereum address format into an array of 5 [`Felt`] values for Miden VM. - /// - /// **Internal API**: This function is used internally during CLAIM note processing to convert - /// the address into the MASM `address[5]` representation expected by the - /// `to_account_id` procedure. - /// - /// The returned order matches the Solidity ABI encoding convention (*big-endian limb order*): - /// - `address[0]` = bytes[0..4] (most-significant 4 bytes, zero for embedded AccountId) - /// - `address[1]` = bytes[4..8] - /// - `address[2]` = bytes[8..12] - /// - `address[3]` = bytes[12..16] - /// - `address[4]` = bytes[16..20] (least-significant 4 bytes) - /// - /// Each limb is interpreted as a little-endian `u32` and stored in a [`Felt`]. - pub fn to_elements(&self) -> Vec { - bytes_to_packed_u32_elements(&self.0) - } - - /// Converts the Ethereum address format back to an [`AccountId`]. - /// - /// **Internal API**: This function is used internally during CLAIM note processing to extract - /// the original AccountId from the Ethereum address format. It mirrors the functionality of - /// the MASM `to_account_id` procedure. - /// - /// # Errors - /// - /// Returns an error if: - /// - the first 4 bytes are not zero (not in the embedded AccountId format), - /// - packing the 8-byte prefix/suffix into [`Felt`] would reduce mod p, - /// - or the resulting felts do not form a valid [`AccountId`]. - pub fn to_account_id(&self) -> Result { - let (prefix, suffix) = Self::bytes20_to_prefix_suffix(self.0)?; - - // Use `Felt::try_from(u64)` to avoid potential truncating conversion - let prefix_felt = - Felt::try_from(prefix).map_err(|_| AddressConversionError::FeltOutOfField)?; - - let suffix_felt = - Felt::try_from(suffix).map_err(|_| AddressConversionError::FeltOutOfField)?; - - AccountId::try_from_elements(suffix_felt, prefix_felt) - .map_err(|_| AddressConversionError::InvalidAccountId) - } - - // HELPER FUNCTIONS - // -------------------------------------------------------------------------------------------- - - /// Convert `[u8; 20]` -> `(prefix, suffix)` by extracting the last 16 bytes. - /// Requires the first 4 bytes be zero. - /// Returns prefix and suffix values that match the MASM little-endian limb byte encoding: - /// - prefix = bytes[4..12] as big-endian u64 = (addr3 << 32) | addr2 - /// - suffix = bytes[12..20] as big-endian u64 = (addr1 << 32) | addr0 - fn bytes20_to_prefix_suffix(bytes: [u8; 20]) -> Result<(u64, u64), AddressConversionError> { - if bytes[0..4] != [0, 0, 0, 0] { - return Err(AddressConversionError::NonZeroBytePrefix); - } - - let prefix = u64::from_be_bytes(bytes[4..12].try_into().unwrap()); - let suffix = u64::from_be_bytes(bytes[12..20].try_into().unwrap()); - - Ok((prefix, suffix)) - } -} - -impl fmt::Display for EthAddressFormat { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_hex()) - } -} - -impl From<[u8; 20]> for EthAddressFormat { - fn from(bytes: [u8; 20]) -> Self { - Self(bytes) - } -} - -impl From for EthAddressFormat { - fn from(account_id: AccountId) -> Self { - EthAddressFormat::from_account_id(account_id) - } -} - -impl From for [u8; 20] { - fn from(addr: EthAddressFormat) -> Self { - addr.0 - } -} - -// ================================================================================================ -// ADDRESS CONVERSION ERROR -// ================================================================================================ - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AddressConversionError { - NonZeroWordPadding, - NonZeroBytePrefix, - InvalidHexLength, - InvalidHexChar(char), - HexParseError, - FeltOutOfField, - InvalidAccountId, -} - -impl fmt::Display for AddressConversionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - AddressConversionError::NonZeroWordPadding => write!(f, "non-zero word padding"), - AddressConversionError::NonZeroBytePrefix => { - write!(f, "address has non-zero 4-byte prefix") - }, - AddressConversionError::InvalidHexLength => { - write!(f, "invalid hex length (expected 40 hex chars)") - }, - AddressConversionError::InvalidHexChar(c) => write!(f, "invalid hex character: {}", c), - AddressConversionError::HexParseError => write!(f, "hex parse error"), - AddressConversionError::FeltOutOfField => { - write!(f, "packed 64-bit word does not fit in the field") - }, - AddressConversionError::InvalidAccountId => write!(f, "invalid AccountId"), - } - } -} - -impl From for AddressConversionError { - fn from(_err: HexParseError) -> Self { - AddressConversionError::HexParseError - } -} diff --git a/crates/miden-agglayer/src/eth_types/eth_address.rs b/crates/miden-agglayer/src/eth_types/eth_address.rs new file mode 100644 index 0000000000..f8a249f110 --- /dev/null +++ b/crates/miden-agglayer/src/eth_types/eth_address.rs @@ -0,0 +1,155 @@ +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::fmt; + +use miden_core::utils::bytes_to_packed_u32_elements; +use miden_protocol::Felt; +use miden_protocol::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes}; + +// ================================================================================================ +// ETHEREUM ADDRESS +// ================================================================================================ + +/// Represents a plain Ethereum address (20 bytes). +/// +/// This is the base type for any 20-byte Ethereum address. It is used for: +/// - Origin token addresses (EVM token contract addresses) +/// - Destination addresses in the bridge-out flow (real Ethereum addresses) +/// - Any other context where a plain 20-byte Ethereum address is needed +/// +/// # Representations used in this module +/// +/// - Raw bytes: `[u8; 20]` in the conventional Ethereum big-endian byte order (`bytes[0]` is the +/// most-significant byte). +/// - MASM "address\[5\]" limbs: 5 x u32 limbs in *big-endian limb order* (each limb encodes its 4 +/// bytes in little-endian order so felts map to keccak bytes directly): +/// - `address[0]` = bytes[0..4] (most-significant 4 bytes) +/// - `address[1]` = bytes[4..8] +/// - `address[2]` = bytes[8..12] +/// - `address[3]` = bytes[12..16] +/// - `address[4]` = bytes[16..20] (least-significant 4 bytes) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EthAddress([u8; 20]); + +impl EthAddress { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`EthAddress`] from a 20-byte array. + pub const fn new(bytes: [u8; 20]) -> Self { + Self(bytes) + } + + /// Creates an [`EthAddress`] from a hex string (with or without "0x" prefix). + /// + /// # Errors + /// + /// Returns an error if the hex string is invalid or the hex part is not exactly 40 characters. + pub fn from_hex(hex_str: &str) -> Result { + let hex_part = hex_str.strip_prefix("0x").unwrap_or(hex_str); + if hex_part.len() != 40 { + return Err(AddressConversionError::InvalidHexLength); + } + + let prefixed_hex = if hex_str.starts_with("0x") { + hex_str.to_string() + } else { + format!("0x{}", hex_str) + }; + + let bytes: [u8; 20] = hex_to_bytes(&prefixed_hex)?; + Ok(Self(bytes)) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns a reference to the underlying 20-byte array. + pub const fn as_bytes(&self) -> &[u8; 20] { + &self.0 + } + + /// Converts the address into a 20-byte array. + pub const fn into_bytes(self) -> [u8; 20] { + self.0 + } + + /// Converts the Ethereum address to a hex string (lowercase, 0x-prefixed). + pub fn to_hex(&self) -> String { + bytes_to_hex_string(self.0) + } + + /// Converts the Ethereum address into an array of 5 [`Felt`] values for Miden VM. + /// + /// The returned order matches the Solidity ABI encoding convention (*big-endian limb order*): + /// - `address[0]` = bytes[0..4] (most-significant 4 bytes) + /// - `address[1]` = bytes[4..8] + /// - `address[2]` = bytes[8..12] + /// - `address[3]` = bytes[12..16] + /// - `address[4]` = bytes[16..20] (least-significant 4 bytes) + /// + /// Each limb is interpreted as a little-endian `u32` and stored in a [`Felt`]. + pub fn to_elements(&self) -> Vec { + bytes_to_packed_u32_elements(&self.0) + } +} + +impl fmt::Display for EthAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_hex()) + } +} + +impl From<[u8; 20]> for EthAddress { + fn from(bytes: [u8; 20]) -> Self { + Self(bytes) + } +} + +impl From for [u8; 20] { + fn from(addr: EthAddress) -> Self { + addr.0 + } +} + +// ================================================================================================ +// ADDRESS CONVERSION ERROR +// ================================================================================================ + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AddressConversionError { + NonZeroWordPadding, + NonZeroBytePrefix, + InvalidHexLength, + InvalidHexChar(char), + HexParseError, + FeltOutOfField, + InvalidAccountId, +} + +impl fmt::Display for AddressConversionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AddressConversionError::NonZeroWordPadding => write!(f, "non-zero word padding"), + AddressConversionError::NonZeroBytePrefix => { + write!(f, "address has non-zero 4-byte prefix") + }, + AddressConversionError::InvalidHexLength => { + write!(f, "invalid hex length (expected 40 hex chars)") + }, + AddressConversionError::InvalidHexChar(c) => write!(f, "invalid hex character: {}", c), + AddressConversionError::HexParseError => write!(f, "hex parse error"), + AddressConversionError::FeltOutOfField => { + write!(f, "packed 64-bit word does not fit in the field") + }, + AddressConversionError::InvalidAccountId => write!(f, "invalid AccountId"), + } + } +} + +impl From for AddressConversionError { + fn from(_err: HexParseError) -> Self { + AddressConversionError::HexParseError + } +} diff --git a/crates/miden-agglayer/src/eth_types/eth_embedded_account_id.rs b/crates/miden-agglayer/src/eth_types/eth_embedded_account_id.rs new file mode 100644 index 0000000000..aa62d04e87 --- /dev/null +++ b/crates/miden-agglayer/src/eth_types/eth_embedded_account_id.rs @@ -0,0 +1,208 @@ +use alloc::string::String; +use alloc::vec::Vec; +use core::fmt; + +use miden_protocol::Felt; +use miden_protocol::account::AccountId; + +use super::eth_address::{AddressConversionError, EthAddress}; + +// ================================================================================================ +// ETH EMBEDDED ACCOUNT ID +// ================================================================================================ + +/// Represents a Miden [`AccountId`] that can be encoded in the 20-byte Ethereum address format. +/// +/// This type wraps an [`AccountId`] and provides conversions to/from the Ethereum address +/// encoding used in the bridge-in flow. In this encoding, the 20-byte Ethereum address format +/// stores a Miden [`AccountId`] as: `0x00000000 || prefix(8) || suffix(8)`, where: +/// - prefix = bytes[4..12] as a big-endian u64 +/// - suffix = bytes[12..20] as a big-endian u64 +/// +/// Note: prefix/suffix are *conceptual* 64-bit words; when converting to [`Felt`], we must ensure +/// `Felt::new(u64)` does not reduce mod p (checked explicitly in [`Self::try_from_eth_address`]). +/// +/// This type is used by integrators (Gateway, claim managers) to convert between Miden AccountIds +/// and the Ethereum address format when constructing CLAIM notes or calling the AggLayer Bridge +/// `bridgeAsset()` function. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EthEmbeddedAccountId(AccountId); + +impl EthEmbeddedAccountId { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates an [`EthEmbeddedAccountId`] from a 20-byte array. + /// + /// The bytes are interpreted as an Ethereum-encoded Miden [`AccountId`] (big-endian): + /// `0x00000000 || prefix(8) || suffix(8)`. + /// + /// # Errors + /// + /// Returns an error if: + /// - the first 4 bytes (i.e., the most significant bytes) are not zero, + /// - packing the 8-byte prefix/suffix into [`Felt`] would reduce mod p, + /// - or the resulting felts do not form a valid [`AccountId`]. + pub fn new(bytes: [u8; 20]) -> Result { + Self::try_from_eth_address(EthAddress::new(bytes)) + } + + /// Creates an [`EthEmbeddedAccountId`] from a hex string (with or without "0x" prefix). + /// + /// # Errors + /// + /// Returns an error if the hex string is invalid, the hex part is not exactly 40 characters, + /// or the decoded bytes do not represent a valid embedded [`AccountId`]. + pub fn from_hex(hex_str: &str) -> Result { + let addr = EthAddress::from_hex(hex_str)?; + Self::try_from_eth_address(addr) + } + + /// Creates an [`EthEmbeddedAccountId`] from an [`AccountId`]. + /// + /// This conversion is infallible: an [`AccountId`] is always valid. + /// + /// # Example + /// ```ignore + /// let embedded = EthEmbeddedAccountId::from_account_id(destination_account_id); + /// let address_bytes = embedded.to_eth_address().into_bytes(); + /// // then construct the CLAIM note with address_bytes... + /// ``` + pub const fn from_account_id(account_id: AccountId) -> Self { + Self(account_id) + } + + /// Creates an [`EthEmbeddedAccountId`] from an [`EthAddress`]. + /// + /// Validates that the address contains a properly encoded Miden [`AccountId`]. + /// + /// # Errors + /// + /// Returns an error if: + /// - the first 4 bytes are not zero (not in the embedded AccountId format), + /// - packing the 8-byte prefix/suffix into [`Felt`] would reduce mod p, + /// - or the resulting felts do not form a valid [`AccountId`]. + pub fn try_from_eth_address(addr: EthAddress) -> Result { + let bytes = addr.into_bytes(); + let (prefix, suffix) = bytes20_to_prefix_suffix(bytes)?; + + let prefix_felt = + Felt::try_from(prefix).map_err(|_| AddressConversionError::FeltOutOfField)?; + + let suffix_felt = + Felt::try_from(suffix).map_err(|_| AddressConversionError::FeltOutOfField)?; + + let account_id = AccountId::try_from_elements(suffix_felt, prefix_felt) + .map_err(|_| AddressConversionError::InvalidAccountId)?; + + Ok(Self(account_id)) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns a reference to the inner [`AccountId`]. + pub const fn to_account_id(&self) -> &AccountId { + &self.0 + } + + /// Consumes self and returns the inner [`AccountId`]. + pub const fn into_account_id(self) -> AccountId { + self.0 + } + + /// Converts the embedded account ID to an [`EthAddress`]. + /// + /// The resulting 20-byte address has the format: + /// `0x00000000 || prefix(8) || suffix(8)` (big-endian byte ordering). + pub fn to_eth_address(&self) -> EthAddress { + let mut out = [0u8; 20]; + out[4..12].copy_from_slice(&self.0.prefix().as_u64().to_be_bytes()); + out[12..20].copy_from_slice(&self.0.suffix().as_canonical_u64().to_be_bytes()); + + EthAddress::new(out) + } + + /// Returns the raw 20-byte Ethereum address encoding. + pub fn to_bytes(&self) -> [u8; 20] { + self.to_eth_address().into_bytes() + } + + /// Converts the address to a hex string (lowercase, 0x-prefixed). + pub fn to_hex(&self) -> String { + self.to_eth_address().to_hex() + } + + /// Converts the address into an array of 5 [`Felt`] values for Miden VM. + /// + /// See [`EthAddress::to_elements`] for details on the encoding. + pub fn to_elements(&self) -> Vec { + self.to_eth_address().to_elements() + } +} + +impl fmt::Display for EthEmbeddedAccountId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_eth_address()) + } +} + +impl TryFrom for EthEmbeddedAccountId { + type Error = AddressConversionError; + + fn try_from(addr: EthAddress) -> Result { + Self::try_from_eth_address(addr) + } +} + +impl From for EthAddress { + fn from(embedded: EthEmbeddedAccountId) -> Self { + embedded.to_eth_address() + } +} + +impl TryFrom<[u8; 20]> for EthEmbeddedAccountId { + type Error = AddressConversionError; + + fn try_from(bytes: [u8; 20]) -> Result { + Self::new(bytes) + } +} + +impl From for [u8; 20] { + fn from(embedded: EthEmbeddedAccountId) -> Self { + embedded.to_bytes() + } +} + +impl From for EthEmbeddedAccountId { + fn from(account_id: AccountId) -> Self { + EthEmbeddedAccountId::from_account_id(account_id) + } +} + +impl From for AccountId { + fn from(embedded: EthEmbeddedAccountId) -> Self { + embedded.0 + } +} + +// ================================================================================================ +// HELPER FUNCTIONS +// ================================================================================================ + +/// Convert `[u8; 20]` -> `(prefix, suffix)` by extracting the last 16 bytes. +/// Requires the first 4 bytes be zero. +/// Returns prefix and suffix values that match the MASM little-endian limb byte encoding: +/// - prefix = bytes[4..12] as big-endian u64 = (addr3 << 32) | addr2 +/// - suffix = bytes[12..20] as big-endian u64 = (addr1 << 32) | addr0 +fn bytes20_to_prefix_suffix(bytes: [u8; 20]) -> Result<(u64, u64), AddressConversionError> { + if bytes[0..4] != [0, 0, 0, 0] { + return Err(AddressConversionError::NonZeroBytePrefix); + } + + let prefix = u64::from_be_bytes(bytes[4..12].try_into().unwrap()); + let suffix = u64::from_be_bytes(bytes[12..20].try_into().unwrap()); + + Ok((prefix, suffix)) +} diff --git a/crates/miden-agglayer/src/eth_types/global_index.rs b/crates/miden-agglayer/src/eth_types/global_index.rs index 35df7e9a85..96aa1f6e09 100644 --- a/crates/miden-agglayer/src/eth_types/global_index.rs +++ b/crates/miden-agglayer/src/eth_types/global_index.rs @@ -1,8 +1,4 @@ -use alloc::vec::Vec; - -use miden_core::utils::bytes_to_packed_u32_elements; -use miden_protocol::Felt; -use miden_protocol::utils::{HexParseError, hex_to_bytes}; +use crate::utils::Keccak256Output; // ================================================================================================ // GLOBAL INDEX ERROR @@ -32,33 +28,43 @@ pub enum GlobalIndexError { /// - 32 bits (limb 7): leaf index (deposit index in the local exit tree) /// /// Bytes are stored in big-endian order, matching Solidity's uint256 representation. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct GlobalIndex([u8; 32]); - -impl GlobalIndex { - /// Creates a [`GlobalIndex`] from a hex string (with or without "0x" prefix). - /// - /// The hex string should represent a Solidity uint256 in big-endian format - /// (64 hex characters for 32 bytes). - pub fn from_hex(hex_str: &str) -> Result { - let bytes: [u8; 32] = hex_to_bytes(hex_str)?; - Ok(Self(bytes)) - } - - /// Creates a new [`GlobalIndex`] from a 32-byte array (big-endian). - pub fn new(bytes: [u8; 32]) -> Self { - Self(bytes) - } +pub type GlobalIndex = Keccak256Output; +/// Extension trait for [`GlobalIndex`] providing AggLayer-specific field accessors and validation. +/// +/// These methods interpret the underlying 32-byte Keccak256 output as a structured global index +/// with mainnet flag, rollup index, and leaf index fields. +#[cfg(any(test, feature = "testing"))] +pub trait GlobalIndexExt { /// Validates this global index. /// /// Checks that: /// - The top 160 bits (bytes 0-19) are zero /// - The mainnet flag (bytes 20-23) is exactly 0 or 1 /// - For mainnet deposits (flag = 1): the rollup index is 0 - pub fn validate(&self) -> Result<(), GlobalIndexError> { + fn validate(&self) -> Result<(), GlobalIndexError>; + + /// Returns the raw mainnet flag value (limb 5, bytes 20-23). + /// + /// Valid values are 0 (rollup) or 1 (mainnet). + fn mainnet_flag(&self) -> u32; + + /// Returns the leaf index (limb 7, lowest 32 bits). + fn leaf_index(&self) -> u32; + + /// Returns the rollup index (limb 6). + fn rollup_index(&self) -> u32; + + /// Returns true if this is a mainnet deposit (mainnet flag = 1). + fn is_mainnet(&self) -> bool; +} + +#[cfg(any(test, feature = "testing"))] +impl GlobalIndexExt for GlobalIndex { + fn validate(&self) -> Result<(), GlobalIndexError> { + let bytes = self.as_bytes(); // Check leading 160 bits are zero - if self.0[0..20].iter().any(|&b| b != 0) { + if bytes[0..20].iter().any(|&b| b != 0) { return Err(GlobalIndexError::LeadingBitsNonZero); } @@ -76,37 +82,24 @@ impl GlobalIndex { Ok(()) } - /// Returns the raw mainnet flag value (limb 5, bytes 20-23). - /// - /// Valid values are 0 (rollup) or 1 (mainnet). - pub fn mainnet_flag(&self) -> u32 { - u32::from_be_bytes([self.0[20], self.0[21], self.0[22], self.0[23]]) + fn mainnet_flag(&self) -> u32 { + let bytes = self.as_bytes(); + u32::from_be_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]) } - /// Returns the leaf index (limb 7, lowest 32 bits). - pub fn leaf_index(&self) -> u32 { - u32::from_be_bytes([self.0[28], self.0[29], self.0[30], self.0[31]]) + fn leaf_index(&self) -> u32 { + let bytes = self.as_bytes(); + u32::from_be_bytes([bytes[28], bytes[29], bytes[30], bytes[31]]) } - /// Returns the rollup index (limb 6). - pub fn rollup_index(&self) -> u32 { - u32::from_be_bytes([self.0[24], self.0[25], self.0[26], self.0[27]]) + fn rollup_index(&self) -> u32 { + let bytes = self.as_bytes(); + u32::from_be_bytes([bytes[24], bytes[25], bytes[26], bytes[27]]) } - /// Returns true if this is a mainnet deposit (mainnet flag = 1). - pub fn is_mainnet(&self) -> bool { + fn is_mainnet(&self) -> bool { self.mainnet_flag() == 1 } - - /// Converts to field elements for note storage / MASM processing. - pub fn to_elements(&self) -> Vec { - bytes_to_packed_u32_elements(&self.0) - } - - /// Returns the raw 32-byte array (big-endian). - pub const fn as_bytes(&self) -> &[u8; 32] { - &self.0 - } } #[cfg(test)] @@ -169,6 +162,8 @@ mod tests { #[test] fn test_mainnet_global_indices_from_production() { + use miden_protocol::Felt; + // Real mainnet global indices from production // Format: (1 << 64) + leaf_index for mainnet deposits // 18446744073709786619 = 0x1_0000_0000_0003_95FB (leaf_index = 235003) diff --git a/crates/miden-agglayer/src/eth_types/mod.rs b/crates/miden-agglayer/src/eth_types/mod.rs index 3bee167e5d..8c401a620a 100644 --- a/crates/miden-agglayer/src/eth_types/mod.rs +++ b/crates/miden-agglayer/src/eth_types/mod.rs @@ -1,9 +1,14 @@ -pub mod address; +pub mod eth_address; +pub mod eth_embedded_account_id; + pub mod amount; pub mod global_index; pub mod metadata_hash; -pub use address::EthAddressFormat; pub use amount::{EthAmount, EthAmountError}; +pub use eth_address::{AddressConversionError, EthAddress}; +pub use eth_embedded_account_id::EthEmbeddedAccountId; +#[cfg(any(test, feature = "testing"))] +pub use global_index::GlobalIndexExt; pub use global_index::{GlobalIndex, GlobalIndexError}; pub use metadata_hash::MetadataHash; diff --git a/crates/miden-agglayer/src/faucet.rs b/crates/miden-agglayer/src/faucet.rs index e7d8367ed0..306a8acc99 100644 --- a/crates/miden-agglayer/src/faucet.rs +++ b/crates/miden-agglayer/src/faucet.rs @@ -27,9 +27,10 @@ pub use crate::{ B2AggNote, ClaimNoteStorage, ConfigAggBridgeNote, - EthAddressFormat, + EthAddress, EthAmount, EthAmountError, + EthEmbeddedAccountId, ExitRoot, GlobalIndex, GlobalIndexError, @@ -94,7 +95,7 @@ static METADATA_HASH_HI_SLOT_NAME: LazyLock = LazyLock::new(|| #[derive(Debug, Clone)] pub struct AggLayerFaucet { metadata: TokenMetadata, - origin_token_address: EthAddressFormat, + origin_token_address: EthAddress, origin_network: u32, scale: u8, metadata_hash: MetadataHash, @@ -117,7 +118,7 @@ impl AggLayerFaucet { decimals: u8, max_supply: Felt, token_supply: Felt, - origin_token_address: EthAddressFormat, + origin_token_address: EthAddress, origin_network: u32, scale: u8, metadata_hash: MetadataHash, @@ -216,7 +217,7 @@ impl AggLayerFaucet { /// - the provided account is not an [`AggLayerFaucet`] account. pub fn origin_token_address( faucet_account: &Account, - ) -> Result { + ) -> Result { // check that the provided account is a faucet account Self::assert_faucet_account(faucet_account)?; @@ -240,7 +241,7 @@ impl AggLayerFaucet { }) .collect::>(); - Ok(EthAddressFormat::new( + Ok(EthAddress::new( addr_bytes_vec .try_into() .expect("origin token addr vector should consist of exactly 20 bytes"), @@ -445,7 +446,7 @@ pub enum AgglayerFaucetError { /// # Returns /// A tuple of two `Word` values representing the two storage slot contents. fn agglayer_faucet_conversion_slots( - origin_token_address: &EthAddressFormat, + origin_token_address: &EthAddress, origin_network: u32, scale: u8, ) -> (Word, Word) { diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 785b0b521e..97ceb05276 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -29,21 +29,35 @@ pub mod errors; pub mod eth_types; pub mod faucet; pub mod update_ger_note; +pub mod utils; pub use b2agg_note::B2AggNote; pub use bridge::{AggLayerBridge, AgglayerBridgeError}; -pub use claim_note::{ClaimNoteStorage, ExitRoot, LeafData, ProofData, SmtNode, create_claim_note}; +pub use claim_note::{ + CgiChainHash, + ClaimNoteStorage, + ExitRoot, + LeafData, + LeafValue, + ProofData, + SmtNode, + create_claim_note, +}; pub use config_note::ConfigAggBridgeNote; +#[cfg(any(test, feature = "testing"))] +pub use eth_types::GlobalIndexExt; pub use eth_types::{ - EthAddressFormat, + EthAddress, EthAmount, EthAmountError, + EthEmbeddedAccountId, GlobalIndex, GlobalIndexError, MetadataHash, }; pub use faucet::{AggLayerFaucet, AgglayerFaucetError}; pub use update_ger_note::UpdateGerNote; +pub use utils::Keccak256Output; // AGGLAYER NOTE SCRIPTS // ================================================================================================ @@ -125,7 +139,7 @@ fn create_agglayer_faucet_component( decimals: u8, max_supply: Felt, token_supply: Felt, - origin_token_address: &EthAddressFormat, + origin_token_address: &EthAddress, origin_network: u32, scale: u8, metadata_hash: MetadataHash, @@ -203,7 +217,7 @@ fn create_agglayer_faucet_builder( max_supply: Felt, token_supply: Felt, bridge_account_id: AccountId, - origin_token_address: &EthAddressFormat, + origin_token_address: &EthAddress, origin_network: u32, scale: u8, metadata_hash: MetadataHash, @@ -237,7 +251,7 @@ pub fn create_agglayer_faucet( decimals: u8, max_supply: Felt, bridge_account_id: AccountId, - origin_token_address: &EthAddressFormat, + origin_token_address: &EthAddress, origin_network: u32, scale: u8, metadata_hash: MetadataHash, @@ -271,7 +285,7 @@ pub fn create_existing_agglayer_faucet( max_supply: Felt, token_supply: Felt, bridge_account_id: AccountId, - origin_token_address: &EthAddressFormat, + origin_token_address: &EthAddress, origin_network: u32, scale: u8, metadata_hash: MetadataHash, diff --git a/crates/miden-agglayer/src/utils.rs b/crates/miden-agglayer/src/utils.rs new file mode 100644 index 0000000000..9211d0e02a --- /dev/null +++ b/crates/miden-agglayer/src/utils.rs @@ -0,0 +1,58 @@ +use alloc::vec::Vec; + +use miden_core::Felt; +#[cfg(any(test, feature = "testing"))] +use miden_core::Word; +use miden_core::utils::bytes_to_packed_u32_elements; +use miden_protocol::utils::{HexParseError, hex_to_bytes}; + +// KECCAK256 OUTPUT +// ================================================================================================ + +/// Keccak256 output representation (32-byte hash) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Keccak256Output([u8; 32]); + +impl Keccak256Output { + /// Creates a new Keccak256 output from a 32-byte array + pub fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Creates a [`Keccak256Output`] from a hex string (with or without "0x" prefix). + /// + /// The hex string should represent 32 bytes (64 hex characters). + pub fn from_hex(hex_str: &str) -> Result { + let bytes: [u8; 32] = hex_to_bytes(hex_str)?; + Ok(Self(bytes)) + } + + /// Returns the inner 32-byte array + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Converts the Keccak256 output to 8 Felt elements (32-byte value as 8 u32 values in + /// little-endian) + pub fn to_elements(&self) -> Vec { + bytes_to_packed_u32_elements(&self.0) + } + + /// Converts the Keccak256 output to two [`Word`]s: `[lo, hi]`. + /// + /// - `lo` contains the first 4 u32-packed felts (bytes 0..16). + /// - `hi` contains the last 4 u32-packed felts (bytes 16..32). + #[cfg(any(test, feature = "testing"))] + pub fn to_words(&self) -> [Word; 2] { + let elements = self.to_elements(); + let lo: [Felt; 4] = elements[0..4].try_into().expect("to_elements returns 8 felts"); + let hi: [Felt; 4] = elements[4..8].try_into().expect("to_elements returns 8 felts"); + [Word::new(lo), Word::new(hi)] + } +} + +impl From<[u8; 32]> for Keccak256Output { + fn from(bytes: [u8; 32]) -> Self { + Self::new(bytes) + } +} diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index f29989d76d..e1b7526844 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -4,12 +4,13 @@ use alloc::slice; use alloc::string::String; use anyhow::Context; -use miden_agglayer::claim_note::Keccak256Output; use miden_agglayer::errors::ERR_CLAIM_ALREADY_SPENT; use miden_agglayer::{ ClaimNoteStorage, ConfigAggBridgeNote, + EthEmbeddedAccountId, ExitRoot, + LeafValue, SmtNode, UpdateGerNote, agglayer_library, @@ -67,7 +68,7 @@ fn merkle_proof_verification_code( let root = ExitRoot::from(hex_to_bytes(&merkle_paths.roots[index]).unwrap()); let [root_lo, root_hi] = root.to_words(); - let leaf = Keccak256Output::from(hex_to_bytes(&merkle_paths.leaves[index]).unwrap()); + let leaf = LeafValue::from(hex_to_bytes(&merkle_paths.leaves[index]).unwrap()); let [leaf_lo, leaf_hi] = leaf.to_words(); format!( @@ -171,10 +172,9 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a // Get the destination account ID from the leaf data. // This requires the destination_address to be in the embedded Miden AccountId format // (first 4 bytes must be zero). - let destination_account_id = leaf_data - .destination_address - .to_account_id() - .expect("destination address is not an embedded Miden AccountId"); + let destination_account_id = EthEmbeddedAccountId::try_from(leaf_data.destination_address) + .expect("destination address is not an embedded Miden AccountId") + .into_account_id(); // For the simulated/rollup case, create the destination account so we can consume the P2ID note let destination_account = if matches!( diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index bcb963ffc3..52c11a6a73 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -5,7 +5,7 @@ use miden_agglayer::{ AggLayerBridge, B2AggNote, ConfigAggBridgeNote, - EthAddressFormat, + EthAddress, ExitRoot, MetadataHash, create_existing_agglayer_faucet, @@ -24,9 +24,9 @@ use miden_standards::note::StandardNote; use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; use miden_tx::utils::hex_to_bytes; -use super::test_utils::SOLIDITY_MMR_FRONTIER_VECTORS; +use super::test_utils::SOLIDITY_MTF_VECTORS; -/// Tests that 32 sequential B2AGG note consumptions match all 32 Solidity MMR roots. +/// Tests that 32 sequential B2AGG note consumptions match all 32 Solidity MTF roots. /// /// This test exercises the complete bridge-out lifecycle: /// 1. Creates a bridge account (empty faucet registry) and an agglayer faucet with conversion @@ -37,13 +37,13 @@ use super::test_utils::SOLIDITY_MMR_FRONTIER_VECTORS; /// - Validates the faucet is registered via `convert_asset` /// - Calls the faucet's `asset_to_origin_asset` via FPI to get the scaled amount, origin token /// address, and origin network -/// - Writes the leaf data and computes the Keccak hash for the MMR +/// - Writes the leaf data and computes the Keccak hash for the Merkle Tree Faucet /// - Creates a BURN note addressed to the faucet /// 5. Verifies the BURN note was created with the correct asset, tag, and script /// 6. Consumes the BURN note with the faucet to burn the tokens #[tokio::test] async fn bridge_out_consecutive() -> anyhow::Result<()> { - let vectors = &*SOLIDITY_MMR_FRONTIER_VECTORS; + let vectors = &*SOLIDITY_MTF_VECTORS; let note_count = 32usize; assert_eq!(vectors.amounts.len(), note_count, "amount vectors should contain 32 entries"); assert_eq!(vectors.roots.len(), note_count, "root vectors should contain 32 entries"); @@ -86,7 +86,7 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { // CREATE AGGLAYER FAUCET ACCOUNT (with conversion metadata for FPI) // -------------------------------------------------------------------------------------------- - let origin_token_address = EthAddressFormat::from_hex(&vectors.origin_token_address) + let origin_token_address = EthAddress::from_hex(&vectors.origin_token_address) .expect("valid shared origin token address"); let origin_network = 64u32; let scale = 0u8; @@ -124,7 +124,7 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { let mut notes = Vec::with_capacity(note_count); for (i, &amount) in expected_amounts.iter().enumerate().take(note_count) { let destination_network = vectors.destination_networks[i]; - let eth_address = EthAddressFormat::from_hex(&vectors.destination_addresses[i]) + let eth_address = EthAddress::from_hex(&vectors.destination_addresses[i]) .expect("valid destination address"); let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount).unwrap().into(); @@ -296,8 +296,8 @@ async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()> // CREATE AGGLAYER FAUCET ACCOUNT (NOT registered in the bridge) // -------------------------------------------------------------------------------------------- - let vectors = &*SOLIDITY_MMR_FRONTIER_VECTORS; - let origin_token_address = EthAddressFormat::new([0u8; 20]); + let vectors = &*SOLIDITY_MTF_VECTORS; + let origin_token_address = EthAddress::new([0u8; 20]); let metadata_hash = MetadataHash::from_token_info( &vectors.token_name, &vectors.token_symbol, @@ -324,9 +324,7 @@ async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()> FungibleAsset::new(faucet.id(), amount.as_canonical_u64()).unwrap().into(); let destination_address = "0x1234567890abcdef1122334455667788990011aa"; - let eth_address = - EthAddressFormat::from_hex(destination_address).expect("valid Ethereum address"); - + let eth_address = EthAddress::from_hex(destination_address).expect("valid Ethereum address"); let b2agg_note = B2AggNote::create( 1u32, // destination_network eth_address, @@ -419,9 +417,7 @@ async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let destination_network = 1u32; let destination_address = "0x1234567890abcdef1122334455667788990011aa"; - let eth_address = - EthAddressFormat::from_hex(destination_address).expect("valid Ethereum address"); - + let eth_address = EthAddress::from_hex(destination_address).expect("valid Ethereum address"); let assets = NoteAssets::new(vec![bridge_asset])?; // Create the B2AGG note with the USER ACCOUNT as the sender. @@ -547,9 +543,7 @@ async fn b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { let destination_network = 1u32; let destination_address = "0x1234567890abcdef1122334455667788990011aa"; - let eth_address = - EthAddressFormat::from_hex(destination_address).expect("valid Ethereum address"); - + let eth_address = EthAddress::from_hex(destination_address).expect("valid Ethereum address"); let assets = NoteAssets::new(vec![bridge_asset])?; // Create the B2AGG note targeting the real bridge account diff --git a/crates/miden-testing/tests/agglayer/config_bridge.rs b/crates/miden-testing/tests/agglayer/config_bridge.rs index 607cc581e6..f4e760ba58 100644 --- a/crates/miden-testing/tests/agglayer/config_bridge.rs +++ b/crates/miden-testing/tests/agglayer/config_bridge.rs @@ -3,7 +3,7 @@ extern crate alloc; use miden_agglayer::{ AggLayerBridge, ConfigAggBridgeNote, - EthAddressFormat, + EthAddress, create_existing_bridge_account, }; use miden_protocol::Felt; @@ -65,7 +65,7 @@ async fn test_config_agg_bridge_registers_faucet() -> anyhow::Result<()> { // CREATE CONFIG_AGG_BRIDGE NOTE // Use a dummy origin token address for this test let origin_token_address = - EthAddressFormat::from_hex("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + EthAddress::from_hex("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let config_note = ConfigAggBridgeNote::create( faucet_to_register, &origin_token_address, diff --git a/crates/miden-testing/tests/agglayer/faucet_helpers.rs b/crates/miden-testing/tests/agglayer/faucet_helpers.rs index d678e0761a..84ea5b226c 100644 --- a/crates/miden-testing/tests/agglayer/faucet_helpers.rs +++ b/crates/miden-testing/tests/agglayer/faucet_helpers.rs @@ -2,7 +2,7 @@ extern crate alloc; use miden_agglayer::{ AggLayerFaucet, - EthAddressFormat, + EthAddress, MetadataHash, create_existing_agglayer_faucet, create_existing_bridge_account, @@ -36,9 +36,8 @@ fn test_faucet_helper_methods() -> anyhow::Result<()> { let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT); let token_supply = Felt::new(123_456); - let origin_token_address = - EthAddressFormat::from_hex("0x0102030405060708090a0b0c0d0e0f1011121314") - .expect("invalid token address"); + let origin_token_address = EthAddress::from_hex("0x0102030405060708090a0b0c0d0e0f1011121314") + .expect("invalid token address"); let origin_network = 42u32; let scale = 6u8; diff --git a/crates/miden-testing/tests/agglayer/leaf_utils.rs b/crates/miden-testing/tests/agglayer/leaf_utils.rs index d2f9d4d23a..87013815d8 100644 --- a/crates/miden-testing/tests/agglayer/leaf_utils.rs +++ b/crates/miden-testing/tests/agglayer/leaf_utils.rs @@ -3,8 +3,7 @@ extern crate alloc; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_agglayer::agglayer_library; -use miden_agglayer::claim_note::Keccak256Output; +use miden_agglayer::{LeafValue, agglayer_library}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; use miden_crypto::SequentialCommit; @@ -191,8 +190,7 @@ async fn get_leaf_value() -> anyhow::Result<()> { let computed_leaf_value: Vec = exec_output.stack[0..8].to_vec(); let expected_leaf_value_bytes: [u8; 32] = hex_to_bytes(&vector.leaf_value).expect("valid leaf value hex"); - let expected_leaf_value: Vec = - Keccak256Output::from(expected_leaf_value_bytes).to_elements(); + let expected_leaf_value: Vec = LeafValue::from(expected_leaf_value_bytes).to_elements(); assert_eq!(computed_leaf_value, expected_leaf_value); Ok(()) diff --git a/crates/miden-testing/tests/agglayer/mmr_frontier.rs b/crates/miden-testing/tests/agglayer/merkle_tree_frontier.rs similarity index 73% rename from crates/miden-testing/tests/agglayer/mmr_frontier.rs rename to crates/miden-testing/tests/agglayer/merkle_tree_frontier.rs index 986520683e..5c2cf4453b 100644 --- a/crates/miden-testing/tests/agglayer/mmr_frontier.rs +++ b/crates/miden-testing/tests/agglayer/merkle_tree_frontier.rs @@ -1,13 +1,13 @@ use alloc::format; use alloc::string::ToString; -use miden_agglayer::claim_note::SmtNode; -use miden_agglayer::{ExitRoot, agglayer_library}; +use miden_agglayer::{ExitRoot, SmtNode, agglayer_library}; use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest}; use miden_protocol::utils::sync::LazyLock; use miden_standards::code_builder::CodeBuilder; use miden_testing::TransactionContextBuilder; -// KECCAK MMR FRONTIER + +// MERKLE TREE FRONTIER // ================================================================================================ static CANONICAL_ZEROS_32: LazyLock> = LazyLock::new(|| { @@ -28,12 +28,12 @@ static CANONICAL_ZEROS_32: LazyLock> = LazyLock::new(|| { zeros_by_height }); -struct KeccakMmrFrontier32 { +struct MerkleTreeFrontier32 { num_leaves: u32, frontier: [Keccak256Digest; TREE_HEIGHT], } -impl KeccakMmrFrontier32 { +impl MerkleTreeFrontier32 { pub fn new() -> Self { Self { num_leaves: 0, @@ -71,15 +71,15 @@ impl KeccakMmrFrontier32 { #[tokio::test] async fn test_append_and_update_frontier() -> anyhow::Result<()> { - let mut mmr_frontier = KeccakMmrFrontier32::<32>::new(); + let mut mtf = MerkleTreeFrontier32::<32>::new(); - let mut source = "use agglayer::bridge::mmr_frontier32_keccak begin".to_string(); + let mut source = "use agglayer::bridge::merkle_tree_frontier 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; + let root = mtf.append_and_update_frontier(leaf); + let num_leaves = mtf.num_leaves; source.push_str(&leaf_assertion_code( SmtNode::new(leaf.into()), @@ -104,18 +104,18 @@ async fn test_append_and_update_frontier() -> anyhow::Result<()> { } #[tokio::test] -async fn test_check_empty_mmr_root() -> anyhow::Result<()> { +async fn test_check_empty_mtf_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 empty_mtf_root = Keccak256::merge(&[zero_31, zero_31]); - let mut source = "use agglayer::bridge::mmr_frontier32_keccak begin".to_string(); + let mut source = "use agglayer::bridge::merkle_tree_frontier begin".to_string(); for round in 1..=32 { - // check that pushing the zero leaves into the MMR doesn't change its root + // check that pushing the zero leaves into the MTF doesn't change its root source.push_str(&leaf_assertion_code( SmtNode::new(zero_leaf.into()), - ExitRoot::new(empty_mmr_root.into()), + ExitRoot::new(empty_mtf_root.into()), round, )); } @@ -137,14 +137,14 @@ async fn test_check_empty_mmr_root() -> anyhow::Result<()> { // SOLIDITY COMPATIBILITY TESTS // ================================================================================================ -// These tests verify that the Rust KeccakMmrFrontier32 implementation produces identical +// These tests verify that the Rust MerkleTreeFrontier32 implementation produces identical // results to the Solidity DepositContractBase.sol implementation. // Test vectors generated from: https://github.com/agglayer/agglayer-contracts // Run `make generate-solidity-test-vectors` to regenerate the test vectors. -use super::test_utils::{SOLIDITY_CANONICAL_ZEROS, SOLIDITY_MMR_FRONTIER_VECTORS}; +use super::test_utils::{SOLIDITY_CANONICAL_ZEROS, SOLIDITY_MTF_VECTORS}; -/// Verifies that the Rust KeccakMmrFrontier32 produces the same canonical zeros as Solidity. +/// Verifies that the Rust MerkleTreeFrontier32 produces the same canonical zeros as Solidity. #[test] fn test_solidity_canonical_zeros_compatibility() { for (height, expected_hex) in SOLIDITY_CANONICAL_ZEROS.canonical_zeros.iter().enumerate() { @@ -159,35 +159,35 @@ fn test_solidity_canonical_zeros_compatibility() { } } -/// Verifies that the Rust KeccakMmrFrontier32 produces the same roots as Solidity's +/// Verifies that the Rust MerkleTreeFrontier32 produces the same roots as Solidity's /// DepositContractBase after adding each leaf. #[test] -fn test_solidity_mmr_frontier_compatibility() { - let v = &*SOLIDITY_MMR_FRONTIER_VECTORS; +fn test_solidity_mtf_compatibility() { + let mtf_vectors = &*SOLIDITY_MTF_VECTORS; // Validate parallel arrays have same length - assert_eq!(v.leaves.len(), v.roots.len()); - assert_eq!(v.leaves.len(), v.counts.len()); + assert_eq!(mtf_vectors.leaves.len(), mtf_vectors.roots.len()); + assert_eq!(mtf_vectors.leaves.len(), mtf_vectors.counts.len()); - let mut mmr_frontier = KeccakMmrFrontier32::<32>::new(); + let mut mtf = MerkleTreeFrontier32::<32>::new(); - for i in 0..v.leaves.len() { - let leaf = Keccak256Digest::try_from(v.leaves[i].as_str()).unwrap(); - let expected_root = Keccak256Digest::try_from(v.roots[i].as_str()).unwrap(); + for i in 0..mtf_vectors.leaves.len() { + let leaf = Keccak256Digest::try_from(mtf_vectors.leaves[i].as_str()).unwrap(); + let expected_root = Keccak256Digest::try_from(mtf_vectors.roots[i].as_str()).unwrap(); - let actual_root = mmr_frontier.append_and_update_frontier(leaf); - let actual_count = mmr_frontier.num_leaves; + let actual_root = mtf.append_and_update_frontier(leaf); + let actual_count = mtf.num_leaves; assert_eq!( - actual_count, v.counts[i], + actual_count, mtf_vectors.counts[i], "leaf count mismatch after adding leaf {}: expected {}, got {}", - v.leaves[i], v.counts[i], actual_count + mtf_vectors.leaves[i], mtf_vectors.counts[i], actual_count ); assert_eq!( actual_root, expected_root, "root mismatch after adding leaf {} (count={}): expected {}, got {:?}", - v.leaves[i], v.counts[i], v.roots[i], actual_root + mtf_vectors.leaves[i], mtf_vectors.counts[i], mtf_vectors.roots[i], actual_root ); } } @@ -205,8 +205,8 @@ fn leaf_assertion_code(leaf: SmtNode, expected_root: ExitRoot, num_leaves: u32) push.{leaf_hi} push.{leaf_lo} - # add this leaf to the MMR frontier - exec.mmr_frontier32_keccak::append_and_update_frontier + # add this leaf to the MTF + exec.merkle_tree_frontier::append_and_update_frontier # => [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] # assert the root correctness after the first leaf was added @@ -215,10 +215,10 @@ fn leaf_assertion_code(leaf: SmtNode, expected_root: ExitRoot, num_leaves: u32) 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" + assert_eqw.err="MTF root (LO) is incorrect" # => [NEW_ROOT_HI, EXPECTED_ROOT_HI, new_leaf_count] - assert_eqw.err="MMR root (HI) is incorrect" + assert_eqw.err="MTF root (HI) is incorrect" # => [new_leaf_count] # assert the new number of leaves diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index 5d84a0cc9d..6f61b354ee 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -5,7 +5,7 @@ mod config_bridge; mod faucet_helpers; mod global_index; mod leaf_utils; -mod mmr_frontier; +mod merkle_tree_frontier; mod solidity_miden_address_conversion; pub mod test_utils; mod update_ger; diff --git a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs index 3b7ba599cc..19168fa338 100644 --- a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs +++ b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs @@ -2,7 +2,7 @@ extern crate alloc; use alloc::sync::Arc; -use miden_agglayer::{EthAddressFormat, agglayer_library}; +use miden_agglayer::{EthEmbeddedAccountId, agglayer_library}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; use miden_processor::advice::AdviceInputs; @@ -54,8 +54,8 @@ async fn execute_program_with_default_host( #[test] fn test_account_id_to_ethereum_roundtrip() { let original_account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); - let eth_address = EthAddressFormat::from_account_id(original_account_id); - let recovered_account_id = eth_address.to_account_id().unwrap(); + let eth_address = EthEmbeddedAccountId::from_account_id(original_account_id); + let recovered_account_id = eth_address.into_account_id(); assert_eq!(original_account_id, recovered_account_id); } @@ -76,8 +76,8 @@ fn test_bech32_to_ethereum_roundtrip() { for (bech32, expected_evm) in test_addresses.iter().zip(evm_addresses.iter()) { let (network_id, account_id) = AccountId::from_bech32(bech32).unwrap(); - let eth = EthAddressFormat::from_account_id(account_id); - let recovered = eth.to_account_id().unwrap(); + let eth = EthEmbeddedAccountId::from_account_id(account_id); + let recovered = eth.into_account_id(); let recovered_bech32 = recovered.to_bech32(network_id); assert_eq!(&account_id, &recovered); @@ -94,8 +94,8 @@ fn test_random_bech32_to_ethereum_roundtrip() { for _ in 0..3 { let account_id = AccountIdBuilder::new().build_with_rng(&mut rng); let bech32_address = account_id.to_bech32(network_id.clone()); - let eth_address = EthAddressFormat::from_account_id(account_id); - let recovered_account_id = eth_address.to_account_id().unwrap(); + let eth_address = EthEmbeddedAccountId::from_account_id(account_id); + let recovered_account_id = eth_address.into_account_id(); let recovered_bech32 = recovered_account_id.to_bech32(network_id.clone()); assert_eq!(account_id, recovered_account_id); @@ -114,7 +114,7 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { ]; for (idx, original_account_id) in test_account_ids.iter().enumerate() { - let eth_address = EthAddressFormat::from_account_id(*original_account_id); + let eth_address = EthEmbeddedAccountId::from_account_id(*original_account_id); let address_felts = eth_address.to_elements().to_vec(); let limbs: Vec = address_felts diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index f68b253738..69506f7a23 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -4,9 +4,10 @@ use alloc::string::String; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_agglayer::claim_note::{Keccak256Output, ProofData, SmtNode}; +use miden_agglayer::claim_note::{ProofData, SmtNode}; use miden_agglayer::{ - EthAddressFormat, + CgiChainHash, + EthAddress, EthAmount, ExitRoot, GlobalIndex, @@ -62,9 +63,10 @@ pub const MERKLE_PROOF_VECTORS_JSON: &str = pub const CANONICAL_ZEROS_JSON: &str = include_str!("../../../miden-agglayer/solidity-compat/test-vectors/canonical_zeros.json"); -/// MMR frontier vectors JSON from the Foundry-generated file. -pub const MMR_FRONTIER_VECTORS_JSON: &str = - include_str!("../../../miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json"); +/// Merkle Tree Frontier (MTF) vectors JSON from the Foundry-generated file. +pub const MTF_VECTORS_JSON: &str = include_str!( + "../../../miden-agglayer/solidity-compat/test-vectors/merkle_tree_frontier_vectors.json" +); // SERDE HELPERS // ================================================================================================ @@ -125,10 +127,10 @@ impl LeafValueVector { pub fn to_leaf_data(&self) -> LeafData { LeafData { origin_network: self.origin_network, - origin_token_address: EthAddressFormat::from_hex(&self.origin_token_address) + origin_token_address: EthAddress::from_hex(&self.origin_token_address) .expect("valid origin token address hex"), destination_network: self.destination_network, - destination_address: EthAddressFormat::from_hex(&self.destination_address) + destination_address: EthAddress::from_hex(&self.destination_address) .expect("valid destination address hex"), amount: EthAmount::from_uint_str(&self.amount).expect("valid amount uint string"), metadata_hash: MetadataHash::new( @@ -177,10 +179,10 @@ impl ProofValueVector { smt_proof_rollup_exit_root: smt_proof_rollup, global_index: GlobalIndex::from_hex(&self.global_index) .expect("valid global index hex"), - mainnet_exit_root: Keccak256Output::new( + mainnet_exit_root: ExitRoot::new( hex_to_bytes(&self.mainnet_exit_root).expect("valid mainnet exit root hex"), ), - rollup_exit_root: Keccak256Output::new( + rollup_exit_root: ExitRoot::new( hex_to_bytes(&self.rollup_exit_root).expect("valid rollup exit root hex"), ), } @@ -214,7 +216,7 @@ pub struct CanonicalZerosFile { pub canonical_zeros: Vec, } -/// Deserialized MMR frontier vectors from Solidity DepositContractV2. +/// Deserialized Merkle Tree Frontier vectors from Solidity DepositContractV2. /// /// Each leaf is produced by `getLeafValue` using the same hardcoded fields as `bridge_out.masm` /// (leafType=0, originNetwork=64), parametrised by @@ -224,7 +226,7 @@ pub struct CanonicalZerosFile { /// /// Amounts are serialized as uint256 values (JSON numbers). #[derive(Debug, Deserialize)] -pub struct MmrFrontierVectorsFile { +pub struct MTFVectorsFile { pub leaves: Vec, pub roots: Vec, pub counts: Vec, @@ -272,10 +274,9 @@ pub static SOLIDITY_CANONICAL_ZEROS: LazyLock = LazyLock::ne serde_json::from_str(CANONICAL_ZEROS_JSON).expect("failed to parse canonical zeros JSON") }); -/// Lazily parsed MMR frontier vectors from the JSON file. -pub static SOLIDITY_MMR_FRONTIER_VECTORS: LazyLock = LazyLock::new(|| { - serde_json::from_str(MMR_FRONTIER_VECTORS_JSON) - .expect("failed to parse MMR frontier vectors JSON") +/// Lazily parsed Merkle Tree frontier (MTF) vectors from the JSON file. +pub static SOLIDITY_MTF_VECTORS: LazyLock = LazyLock::new(|| { + serde_json::from_str(MTF_VECTORS_JSON).expect("failed to parse MTF vectors JSON") }); // HELPER FUNCTIONS @@ -293,8 +294,8 @@ pub enum ClaimDataSource { } impl ClaimDataSource { - /// Returns the `(ProofData, LeafData, ExitRoot)` tuple for this data source. - pub fn get_data(self) -> (ProofData, LeafData, ExitRoot, Keccak256Output) { + /// Returns the `(ProofData, LeafData, ExitRoot, CgiChainHash)` tuple for this data source. + pub fn get_data(self) -> (ProofData, LeafData, ExitRoot, CgiChainHash) { let vector = match self { ClaimDataSource::Real => &*CLAIM_ASSET_VECTOR, ClaimDataSource::Simulated => &*CLAIM_ASSET_VECTOR_LOCAL, @@ -303,7 +304,7 @@ impl ClaimDataSource { let ger = ExitRoot::new( hex_to_bytes(&vector.proof.global_exit_root).expect("valid global exit root hex"), ); - let cgi_chain_hash = Keccak256Output::new( + let cgi_chain_hash = CgiChainHash::new( hex_to_bytes(&vector.proof.claimed_global_index_hash_chain) .expect("invalid CGI chain hash"), );