From 0ab37e0a244d832893b8ae7f3834eff6f71867a4 Mon Sep 17 00:00:00 2001 From: Marti Date: Tue, 3 Mar 2026 09:30:53 +0000 Subject: [PATCH 1/4] chore: bump workspace version to 0.14.0-alpha.2 --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e009750dbd..d60f168d6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1381,7 +1381,7 @@ dependencies = [ [[package]] name = "miden-agglayer" -version = "0.14.0-alpha.1" +version = "0.14.0-alpha.2" dependencies = [ "fs-err", "miden-agglayer", @@ -1452,7 +1452,7 @@ dependencies = [ [[package]] name = "miden-block-prover" -version = "0.14.0-alpha.1" +version = "0.14.0-alpha.2" dependencies = [ "miden-protocol", "thiserror", @@ -1644,7 +1644,7 @@ dependencies = [ [[package]] name = "miden-protocol" -version = "0.14.0-alpha.1" +version = "0.14.0-alpha.2" dependencies = [ "anyhow", "assert_matches", @@ -1683,7 +1683,7 @@ dependencies = [ [[package]] name = "miden-protocol-macros" -version = "0.14.0-alpha.1" +version = "0.14.0-alpha.2" dependencies = [ "miden-protocol", "proc-macro2", @@ -1707,7 +1707,7 @@ dependencies = [ [[package]] name = "miden-standards" -version = "0.14.0-alpha.1" +version = "0.14.0-alpha.2" dependencies = [ "anyhow", "assert_matches", @@ -1726,7 +1726,7 @@ dependencies = [ [[package]] name = "miden-testing" -version = "0.14.0-alpha.1" +version = "0.14.0-alpha.2" dependencies = [ "anyhow", "assert_matches", @@ -1756,7 +1756,7 @@ dependencies = [ [[package]] name = "miden-tx" -version = "0.14.0-alpha.1" +version = "0.14.0-alpha.2" dependencies = [ "anyhow", "assert_matches", @@ -1773,7 +1773,7 @@ dependencies = [ [[package]] name = "miden-tx-batch-prover" -version = "0.14.0-alpha.1" +version = "0.14.0-alpha.2" dependencies = [ "miden-protocol", "miden-tx", diff --git a/Cargo.toml b/Cargo.toml index f6fd47a315..ad9288015a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ homepage = "https://miden.xyz" license = "MIT" repository = "https://github.com/0xMiden/protocol" rust-version = "1.90" -version = "0.14.0-alpha.1" +version = "0.14.0-alpha.2" [profile.release] codegen-units = 1 @@ -42,14 +42,14 @@ lto = true [workspace.dependencies] # Workspace crates -miden-agglayer = { default-features = false, path = "crates/miden-agglayer", version = "=0.14.0-alpha.1" } -miden-block-prover = { default-features = false, path = "crates/miden-block-prover", version = "=0.14.0-alpha.1" } -miden-protocol = { default-features = false, path = "crates/miden-protocol", version = "=0.14.0-alpha.1" } -miden-protocol-macros = { default-features = false, path = "crates/miden-protocol-macros", version = "=0.14.0-alpha.1" } -miden-standards = { default-features = false, path = "crates/miden-standards", version = "=0.14.0-alpha.1" } -miden-testing = { default-features = false, path = "crates/miden-testing", version = "=0.14.0-alpha.1" } -miden-tx = { default-features = false, path = "crates/miden-tx", version = "=0.14.0-alpha.1" } -miden-tx-batch-prover = { default-features = false, path = "crates/miden-tx-batch-prover", version = "=0.14.0-alpha.1" } +miden-agglayer = { default-features = false, path = "crates/miden-agglayer", version = "=0.14.0-alpha.2" } +miden-block-prover = { default-features = false, path = "crates/miden-block-prover", version = "=0.14.0-alpha.2" } +miden-protocol = { default-features = false, path = "crates/miden-protocol", version = "=0.14.0-alpha.2" } +miden-protocol-macros = { default-features = false, path = "crates/miden-protocol-macros", version = "=0.14.0-alpha.2" } +miden-standards = { default-features = false, path = "crates/miden-standards", version = "=0.14.0-alpha.2" } +miden-testing = { default-features = false, path = "crates/miden-testing", version = "=0.14.0-alpha.2" } +miden-tx = { default-features = false, path = "crates/miden-tx", version = "=0.14.0-alpha.2" } +miden-tx-batch-prover = { default-features = false, path = "crates/miden-tx-batch-prover", version = "=0.14.0-alpha.2" } # Miden dependencies miden-air = { default-features = false, version = "0.20" } From 83ccb2fdaac95900f1671c3f94f0ee2c2e0b3e14 Mon Sep 17 00:00:00 2001 From: Marti Date: Wed, 4 Mar 2026 15:57:42 +0100 Subject: [PATCH 2/4] feat(AggLayer): bridging spec (#2469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: new SPEC file * feat: entities and permissions * feat: contracts and public interfaces * feat: integrate contract storage into sec 2. * feat: notes, incl properties, storage, consumption * feat: match Rust fields for note spec * docs: update SPEC.md for resolved issues and current bridge design * docs: address review comments on SPEC.md - Update baseline to "to-be-tagged v0.14-alpha" - Add explanation for why native claim amount is verified (avoids expensive U256 division inside the VM) - Simplify addr output notation to addr(5) https://claude.ai/code/session_01UDgsAS2j2CFrTLsDoLiSUN * chore: update docs to match latest agglayer branch * docs(AggLayer): Ethereum <> Miden address conversion specification (#2513) * docs: add Section 5 — Ethereum ↔ Miden address conversion spec Comprehensive specification of the address conversion encoding between Ethereum 20-byte addresses and Miden AccountId (two field elements), covering the embedded format, MASM limb representation, Rust and MASM conversion procedures, endianness details, and roundtrip guarantees. Addresses https://github.com/0xMiden/protocol/issues/2229 https://claude.ai/code/session_01YabAhXZeStAkKkYwBcXXFh * Apply suggestions from code review Update crates/miden-agglayer/SPEC.md Update crates/miden-agglayer/SPEC.md Update crates/miden-agglayer/SPEC.md * fix: EthAddressFormat::from_account_id usage * docs: clean up address conversion --------- Co-authored-by: Claude * chore: add sec 4. placeholder * chore: add a note about encoding of u32s in note storage --------- Co-authored-by: Claude --- crates/miden-agglayer/SPEC.md | 692 ++++++++++++++++++ .../asm/agglayer/common/eth_address.masm | 2 +- 2 files changed, 693 insertions(+), 1 deletion(-) create mode 100644 crates/miden-agglayer/SPEC.md diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md new file mode 100644 index 0000000000..57a33b3f91 --- /dev/null +++ b/crates/miden-agglayer/SPEC.md @@ -0,0 +1,692 @@ +# AggLayer <> Miden Bridge Integration Specification + +**Scope:** Implementation-accurate specification of the AggLayer bridge integration on +Miden, covering contracts, note flows, storage, and encoding semantics. + +**Baseline:** Branch `agglayer` (to-be-tagged `v0.14-alpha`). All statements in sections 1-3 describe +current implementation behaviour and are cross-checked against the test suite in +`crates/miden-testing/tests/agglayer/`. Planned changes that diverge from the current +implementation are called out inline with `TODO (Future)` markers. + +**Conventions:** + +- *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. + +--- + +## 1. Entities and Trust Model + +| Entity | Description | Account type | +|--------|-------------|--------------| +| **User** | End-user Miden account that holds assets and initiates bridge-out deposits, or receives assets from a bridge-in claim. | Any account with `basic_wallet` component | +| **AggLayer Bridge** | Onchain bridge account that manages the Local Exit Tree (LET), faucet registry, and GER state. Consumes B2AGG, CONFIG, and UPDATE_GER notes. | Network-mode account with a single `bridge` component | +| **AggLayer Faucet** | Fungible faucet that represents a single bridged token. Mints on bridge-in claims, burns on bridge-out. Each foreign token has its own faucet instance. | `FungibleFaucet`, network-mode, with `agglayer_faucet` component | +| **Integration Service** (offchain) | Observes L1 events (deposits, GER updates) and creates UPDATE_GER and CLAIM notes on Miden. Trusted to provide correct proofs and data. | Not an onchain entity; creates notes targeting bridge/faucet | +| **Bridge Operator** (offchain) | Deploys bridge and faucet accounts. Creates CONFIG_AGG_BRIDGE notes to register faucets. Must use the bridge admin account. | Not an onchain entity; creates config notes | + +### Current permissions + +| Note type | Issuer (sender check) | Consumer (consuming-account check) | +|-----------|----------------------|-----------------------------------| +| B2AGG (bridge-out) | Any user -- not restricted | Bridge account -- **enforced** via `NetworkAccountTarget` attachment | +| 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 | + +--- + +## 2. Contracts and Public Interfaces + +### 2.1 Bridge Account Component + +The bridge account has a single unified `bridge` component (`components/bridge.masm`), +which is a thin wrapper that re-exports procedures from the `agglayer` library modules: + +- `bridge_config::register_faucet` +- `bridge_config::update_ger` +- `bridge_in::verify_leaf_bridge` +- `bridge_out::bridge_out` + +The underlying library code lives in `asm/agglayer/bridge/` with supporting modules in +`asm/agglayer/common/`. + +#### `bridge_out::bridge_out` + +| | | +|-|-| +| **Invocation** | `call` | +| **Inputs** | `[ASSET, dest_network_id, dest_addr(5), pad(4)]` | +| **Outputs** | `[]` | +| **Context** | Consuming a `B2AGG` note on the bridge account | +| **Panics** | Faucet not in registry; FPI to faucet fails | + +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). +5. Creates a public `BURN` note targeting the faucet via a `NetworkAccountTarget` attachment. + +#### `bridge_config::register_faucet` + +| | | +|-|-| +| **Invocation** | `call` | +| **Inputs** | `[faucet_id_prefix, faucet_id_suffix, pad(14)]` | +| **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 +`miden::agglayer::bridge::admin`, then writes +`[0, 0, faucet_id_suffix, faucet_id_prefix] -> [1, 0, 0, 0]` into the +`faucet_registry` map slot. + +#### `bridge_config::update_ger` + +| | | +|-|-| +| **Invocation** | `call` | +| **Inputs** | `[GER_LOWER(4), GER_UPPER(4), pad(8)]` | +| **Outputs** | `[pad(16)]` | +| **Context** | Consuming an `UPDATE_GER` note on the bridge account | +| **Panics** | Note sender is not the GER manager | + +Asserts the note sender matches the GER manager stored in +`miden::agglayer::bridge::ger_manager`, then computes +`KEY = rpo256::merge(GER_UPPER, GER_LOWER)` and stores +`KEY -> [1, 0, 0, 0]` in the `ger` map slot. This marks the GER as "known". + +#### `bridge_in::verify_leaf_bridge` + +| | | +|-|-| +| **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 | +| **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`. + +#### Bridge Account Storage + +| Slot name | Slot type | Key encoding | Value encoding | Purpose | +|-----------|-----------|-------------|----------------|---------| +| `miden::agglayer::bridge::ger` | Map | `rpo256::merge(GER_UPPER, GER_LOWER)` | `[1, 0, 0, 0]` if known; `[0, 0, 0, 0]` if absent | Known Global Exit Root set | +| `miden::agglayer::let` | 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 | +| `miden::agglayer::let::root_lo` | Value | -- | `[root_0, root_1, root_2, root_3]` | LET root low word (Keccak-256 lower 16 bytes) | +| `miden::agglayer::let::root_hi` | Value | -- | `[root_4, root_5, root_6, root_7]` | LET root high word (Keccak-256 upper 16 bytes) | +| `miden::agglayer::let::num_leaves` | Value | -- | `[count, 0, 0, 0]` | Number of leaves appended to the LET | +| `miden::agglayer::bridge::faucet_registry` | Map | `[0, 0, faucet_id_suffix, faucet_id_prefix]` | `[1, 0, 0, 0]` if registered; `[0, 0, 0, 0]` if absent | Registered faucet lookup | +| `miden::agglayer::bridge::admin` | Value | -- | `[0, 0, admin_suffix, admin_prefix]` | Bridge admin account ID for CONFIG note authorization | +| `miden::agglayer::bridge::ger_manager` | Value | -- | `[0, 0, mgr_suffix, mgr_prefix]` | GER manager account ID for UPDATE_GER note authorization | + +Initial state: all map slots empty, all value slots `[0, 0, 0, 0]` except `admin` and +`ger_manager` which are set at account creation time. + +### 2.2 Faucet Account Component + +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::asset_to_origin_asset` +- `faucet::burn` + +The underlying library code lives in `asm/agglayer/faucet/mod.masm` with supporting +modules in `asm/agglayer/common/`. + +#### `agglayer_faucet::claim` + +| | | +|-|-| +| **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 | + +Processes a bridge-in claim: + +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::asset_to_origin_asset` + +| | | +|-|-| +| **Invocation** | `call` (invoked via FPI from the bridge) | +| **Inputs** | `[amount, pad(15)]` | +| **Outputs** | `[AMOUNT_U256_0(4), AMOUNT_U256_1(4), addr(5), origin_network, pad(2)]` | +| **Context** | FPI target -- called by the bridge during bridge-out | +| **Panics** | Scale exceeds 18 | + +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. + +#### `agglayer_faucet::burn` + +This is a re-export of `miden::standards::faucets::basic_fungible::burn`. It burns the fungible asset from the active note, decreasing the faucet's token supply. + +| | | +|-|-| +| **Invocation** | `call` | +| **Inputs** | `[pad(16)]` | +| **Outputs** | `[pad(16)]` | +| **Context** | Consuming a `BURN` note on the faucet account | +| **Panics** | Note context invalid; asset count wrong; faucet/supply checks fail | + +#### Faucet Account Storage + +| Slot name | Slot type | Value encoding | Purpose | +|-----------|-----------|----------------|---------| +| Faucet metadata (standard) | Value | `[token_supply, max_supply, decimals, token_symbol]` | Standard `NetworkFungibleFaucet` metadata | +| `miden::agglayer::faucet` (TODO (Future): rename for clarity [#2356](https://github.com/0xMiden/protocol/issues/2356)) | Value | `[0, 0, bridge_suffix, bridge_prefix]` | Bridge account ID this faucet is paired with | +| `miden::agglayer::faucet::conversion_info_1` | Value | `[addr_0, addr_1, addr_2, addr_3]` | Origin token address, first 4 u32 limbs | +| `miden::agglayer::faucet::conversion_info_2` | Value | `[addr_4, origin_network, scale, 0]` | Origin token address 5th limb, origin network ID, scale exponent | + +--- + +## 3. Note Types and Storage Layouts + +**Encoding conventions:** All multi-byte values in note storage (addresses, U256 +integers, Keccak-256 hashes) are encoded as arrays of u32 felts via +`bytes_to_packed_u32_felts`: big-endian limb order with **little-endian byte order** +within each 4-byte limb (see [Section 5.5](#55-endianness-summary)). Scalar u32 fields +(network IDs) are byte-reversed at storage time so their in-memory bytes align with the +Keccak preimage format directly — the felt value does **not** equal the numeric value +(e.g., chain ID `1` = `0x00000001` is stored as felt `0x01000000`). + +### 3.1 B2AGG +(Bridge-to-AggLayer) + +**Purpose:** User bridges an asset from Miden to the AggLayer. + +**`NoteHeader`** + +*`NoteMetadata`:* + +| Field | Value | +|-------|-------| +| `sender` | Any account (not validated) | +| `note_type` | `NoteType::Public` | +| `tag` | `NoteTag::default()` | +| `attachment` | `NetworkAccountTarget` -- target is the bridge account; execution hint: Always | + +**`NoteDetails`** + +*`NoteAssets`:* Exactly 1 fungible asset. + +*`NoteRecipient`:* + +| Field | Value | +|-------|-------| +| `serial_num` | Random (`rng.draw_word()`) | +| `script` | `B2AGG.masb` | +| `storage` | 6 felts -- see layout below | + +**Storage layout (6 felts):** + +| Index | Field | Encoding | +|-------|-------|----------| +| 0 | `destination_network` | u32 | +| 1-5 | `destination_address` | 5 x u32 felts (20-byte Ethereum address) | + +**Consumption:** + +- **Bridge-out:** Consuming account is the bridge -> note validates attachment target, + loads storage and asset, calls `bridge_out::bridge_out`. +- **Reclaim:** Consuming account is the original sender -> assets are added back to the + account via `basic_wallet::add_assets_to_account`. No output notes. + +### 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. + +**`NoteHeader`** + +*`NoteMetadata`:* + +| Field | Value | +|-------|-------| +| `sender` | Any account (not validated) | +| `note_type` | `NoteType::Public` | +| `tag` | `NoteTag::default()` | +| `attachment` | `NetworkAccountTarget` -- target is the faucet account; execution hint: Always | + +**`NoteDetails`** + +*`NoteAssets`:* None (empty). + +*`NoteRecipient`:* + +| Field | Value | +|-------|-------| +| `serial_num` | Random (`rng.draw_word()`) | +| `script` | `CLAIM.masb` | +| `storage` | 569 felts -- see layout below | + +**Storage layout (569 felts):** + +The storage is divided into three logical regions: proof data (felts 0-535), leaf data +(felts 536-567), and the native claim amount (felt 568). + +| Range | Field | Size (felts) | Encoding | +|-------|-------|-------------|----------| +| 0-255 | `smt_proof_local_exit_root` | 256 | 32 x Keccak-256 nodes (8 felts each) | +| 256-511 | `smt_proof_rollup_exit_root` | 256 | 32 x Keccak-256 nodes (8 felts each) | +| 512-519 | `global_index` | 8 | U256 as 8 x u32 felts | +| 520-527 | `mainnet_exit_root` | 8 | Keccak-256 hash as 8 x u32 felts | +| 528-535 | `rollup_exit_root` | 8 | Keccak-256 hash as 8 x u32 felts | +| 536 | `leaf_type` | 1 | u32 (0 = asset) | +| 537 | `origin_network` | 1 | u32 | +| 538-542 | `origin_token_address` | 5 | 5 x u32 felts | +| 543 | `destination_network` | 1 | u32 | +| 544-548 | `destination_address` | 5 | 5 x u32 felts | +| 549-556 | `amount` | 8 | U256 as 8 x u32 felts | +| 557-564 | `metadata_hash` | 8 | Keccak-256 hash as 8 x u32 felts | +| 565-567 | padding | 3 | zeros | +| 568 | `miden_claim_amount` | 1 | Scaled-down Miden token amount (Felt). Computed as `floor(amount / 10^scale)` | + +**Consumption:** + +1. Script asserts consuming account matches the target faucet via `NetworkAccountTarget` + attachment (checked before loading storage). +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.3 CONFIG_AGG_BRIDGE + +**Purpose:** Registers a faucet in the bridge's faucet registry. + +**`NoteHeader`** + +*`NoteMetadata`:* + +| Field | Value | +|-------|-------| +| `sender` | Bridge admin (sender authorization enforced by the bridge's `register_faucet` procedure) | +| `note_type` | `NoteType::Public` | +| `tag` | `NoteTag::default()` | +| `attachment` | `NetworkAccountTarget` -- target is the bridge account; execution hint: Always | + +**`NoteDetails`** + +*`NoteAssets`:* None (empty). + +*`NoteRecipient`:* + +| Field | Value | +|-------|-------| +| `serial_num` | Random (`rng.draw_word()`) | +| `script` | `CONFIG_AGG_BRIDGE.masb` | +| `storage` | 2 felts -- see layout below | + +**Storage layout (2 felts):** + +| Index | Field | Encoding | +|-------|-------|----------| +| 0 | `faucet_id_prefix` | Felt (AccountId prefix) | +| 1 | `faucet_id_suffix` | Felt (AccountId suffix) | + +**Consumption:** Script validates attachment target, loads storage, and calls +`bridge_config::register_faucet` (which asserts sender is bridge admin). + +### 3.4 UPDATE_GER + +**Purpose:** Stores a new Global Exit Root (GER) in the bridge account so that subsequent +CLAIM notes can be verified against it. + +**`NoteHeader`** + +*`NoteMetadata`:* + +| Field | Value | +|-------|-------| +| `sender` | GER manager (sender authorization enforced by the bridge's `update_ger` procedure) | +| `note_type` | `NoteType::Public` | +| `tag` | `NoteTag::default()` | +| `attachment` | `NetworkAccountTarget` -- target is the bridge account; execution hint: Always | + +**`NoteDetails`** + +*`NoteAssets`:* None (empty). + +*`NoteRecipient`:* + +| Field | Value | +|-------|-------| +| `serial_num` | Random (`rng.draw_word()`) | +| `script` | `UPDATE_GER.masb` | +| `storage` | 8 felts -- see layout below | + +**Storage layout (8 felts):** + +| Range | Field | Encoding | +|-------|-------|----------| +| 0-3 | `GER_LOWER` | First 16 bytes as 4 x u32 felts | +| 4-7 | `GER_UPPER` | Last 16 bytes as 4 x u32 felts | + +**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. + +### 3.5 BURN (generated) + +**Purpose:** Created by `bridge_out::bridge_out` to burn the bridged asset on the faucet. + +**`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`:* The single fungible asset from the originating B2AGG note. + +*`NoteRecipient`:* + +| Field | Value | +|-------|-------| +| `serial_num` | Derived as `rpo256::merge(B2AGG_SERIAL_NUM, ASSET)` | +| `script` | Standard BURN script (`miden::standards::notes::burn::main`) | +| `storage` | None (0 felts) | + +**Storage layout (0 felts):** + +No fields -- this is a standard burn note with no custom data. + +**Consumption:** + +The standard BURN script calls `faucets::burn` on the consuming faucet account. This +validates that the note contains exactly one fungible asset issued by that faucet and +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. + +**`NoteHeader`** + +*`NoteMetadata`:* + +| Field | Value | +|-------|-------| +| `sender` | Faucet account | +| `note_type` | `NoteType::Public` | +| `tag` | Computed at runtime from destination account prefix via `note_tag::create_account_target` | +| `attachment` | None | + +**`NoteDetails`** + +*`NoteAssets`:* The minted fungible asset for the claim amount. + +*`NoteRecipient`:* + +| Field | Value | +|-------|-------| +| `serial_num` | Derived deterministically from `PROOF_DATA_KEY` (RPO256 hash of the CLAIM proof data) | +| `script` | Standard P2ID script (`miden::standards::notes::p2id::main`) | +| `storage` | 2 felts -- see layout below | + +**Storage layout (2 felts):** + +| Index | Field | Encoding | +|-------|-------|----------| +| 0 | `target_account_id_prefix` | Felt (AccountId prefix) | +| 1 | `target_account_id_suffix` | Felt (AccountId suffix) | + +**Consumption:** + +Consuming account must match `target_account_id` from note storage (enforced by the P2ID +script). All note assets are added to the consuming account via +`basic_wallet::add_assets_to_account`. + +--- + +## 4. Amount Conversion + +*This section is a placeholder. Content to be added.* + +--- + +## 5. Ethereum ↔ Miden Address Conversion + +The AggLayer bridge operates across two address spaces: Ethereum's 20-byte addresses and +Miden's `AccountId` (two field elements). This section specifies the encoding that maps +between them, as implemented in Rust (`eth_types/address.rs`) and MASM +(`agglayer/common/eth_address.masm`). + +### 5.1 Background + +Miden's `AccountId` (version 0) consists of two Goldilocks field elements: + +```text +prefix: [hash (56 bits) | storage_mode (2 bits) | type (2 bits) | version (4 bits)] +suffix: [zero_bit | hash (55 bits) | 8 zero_bits] +``` + +Each element is a `u64` value less than the Goldilocks prime `p = 2^64 − 2^32 + 1`, +giving a combined 120 bits of entropy. A prefix is always a valid felt because it derives +directly from a hash output; the suffix's MSB is constrained to zero and its lower 8 bits +are zeroed. + +Ethereum addresses are 20-byte (160-bit) values. Because every valid `AccountId` fits in +16 bytes (prefix: 8 bytes, suffix: 8 bytes), it can be embedded into the lower 16 bytes +of an Ethereum address with 4 zero-padding bytes at the top. + +### 5.2 Embedded Format + +An `AccountId` is embedded in a 20-byte Ethereum address as follows: + +```text + Byte offset: 0 4 8 12 16 20 + ┌────┬─────────┬─────────┐ + │0000│ prefix │ suffix │ + └────┴─────────┴─────────┘ + 4B 8B 8B +``` + +| Byte range | Content | Encoding | +|------------|---------|----------| +| `[0..4)` | Zero padding | Must be `0x00000000` | +| `[4..12)` | `prefix` | Big-endian `u64` (`felts[0].as_int().to_be_bytes()`) | +| `[12..20)` | `suffix` | Big-endian `u64` (`felts[1].as_int().to_be_bytes()`) | + +**Example conversions:** + +| Bech32 | Ethereum address | +|--------|-----------------| +| `mtst1azcw08rget79fqp8ymr0zqkv5v5lj466` | `0x00000000b0e79c68cafc54802726c6f102cca300` | +| `mtst1arxmxavamh7lqyp79mexktt4vgxv40mp` | `0x00000000cdb3759dddfdf0103e2ef26b2d756200` | +| `mtst1ar2phe0pa0ln75plsczxr8ryws4s8zyp` | `0x00000000d41be5e1ebff3f503f8604619c647400` | + +Note that the last byte of the Ethereum address is always `0x00` because the lower 8 bits +of the `AccountId` suffix are always zero. + +**Limitation:** Not all Ethereum addresses are valid Miden accounts. The conversion from +Ethereum address to `AccountId` is partial — it fails if the leading 4 bytes are +non-zero, if the packed `u64` values exceed the field modulus, or if the resulting felts +don't form a valid `AccountId`. Arbitrary Ethereum addresses (e.g., from EOAs or +contracts on L1) cannot generally be decoded into `AccountId` values. + +### 5.3 MASM Limb Representation + +Inside the Miden VM, a 20-byte Ethereum address is represented as 5 field elements, each +holding a `u32` value. This layout uses **big-endian limb +order** (matching the Solidity ABI encoding convention): + +| Limb | Byte range | Description | +|------|-----------|-------------| +| `address[0]` | `bytes[0..4]` | Most-significant 4 bytes (must be zero for embedded `AccountId`) | +| `address[1]` | `bytes[4..8]` | Upper half of prefix | +| `address[2]` | `bytes[8..12]` | Lower half of prefix | +| `address[3]` | `bytes[12..16]` | Upper half of suffix | +| `address[4]` | `bytes[16..20]` | Lower half of suffix | + +**Byte order within each limb:** Each 4-byte chunk is packed into a `u32` felt using +**little-endian** byte order, aligning with the expected format for the +Keccak-256 precompile. + +The Rust function `EthAddressFormat::to_elements()` produces exactly this 5-felt array +from a 20-byte address. + +### 5.4 Conversion Procedures + +#### 5.4.1 `AccountId` → Ethereum Address (Rust) + +`EthAddressFormat::from_account_id(account_id: AccountId) -> EthAddressFormat` + +This is the **external API** used by the bridge interface. It lets a user convert a Miden `AccountId` (destination account on Miden) into an Ethereum address that will be encoded in the deposit data. + +**Algorithm:** + +1. Extract the two felts from the `AccountId`: `[prefix_felt, suffix_felt]`. +2. Write the prefix felt's `u64` value as 8 big-endian bytes into `out[4..12]`. +3. Write the suffix felt's `u64` value as 8 big-endian bytes into `out[12..20]`. +4. Leave `out[0..4]` as zeros. + +This conversion is **infallible**: every valid `AccountId` produces a valid 20-byte +address. + +#### 5.4.2 Ethereum Address → `AccountId` (Rust) + +`EthAddressFormat::to_account_id(&self) -> Result` + +This is used internally during CLAIM note processing to extract the recipient's +`AccountId` from the embedded Ethereum address. + +While currently this is only used for testing purposes, the claim manager service could use this to +extract the recipient's `AccountId` from the embedded Ethereum address and e.g. perform some checks on the receiving account, such as checking if the account is new or already has funds. + +**Algorithm:** + +1. Assert `bytes[0..4] == [0, 0, 0, 0]`. Error: `NonZeroBytePrefix`. +2. Read `prefix = u64::from_be_bytes(bytes[4..12])`. +3. Read `suffix = u64::from_be_bytes(bytes[12..20])`. +4. Convert each `u64` to a `Felt` via `Felt::try_from(u64)`. Error: `FeltOutOfField` if + the value ≥ p (would be reduced mod p). +5. Construct `AccountId::try_from([prefix_felt, suffix_felt])`. Error: `InvalidAccountId` + if the felts don't satisfy `AccountId` constraints (invalid version, type, storage + mode, or suffix shape). + +**Error conditions:** + +| Error | Condition | +|-------|-----------| +| `NonZeroBytePrefix` | First 4 bytes are not zero | +| `FeltOutOfField` | A `u64` value ≥ the Goldilocks prime `p` | +| `InvalidAccountId` | The resulting felts don't form a valid `AccountId` | + +#### 5.4.3 Ethereum Address → `AccountId` (MASM) + +`eth_address::to_account_id` — Module: `miden::agglayer::common::eth_address` + +This is the in-VM counterpart of the Rust `to_account_id`, invoked during CLAIM note +consumption to decode the recipient's address from the leaf data, and eventually for building the P2ID note for the recipient. + +**Stack signature:** + +```text +Inputs: [limb0, limb1, limb2, limb3, limb4] +Outputs: [prefix, suffix] +Invocation: exec +``` + +**Algorithm:** + +1. `assertz limb0` — the most-significant limb must be zero (error: `ERR_MSB_NONZERO`). +2. Build `suffix` from `(limb4, limb3)`: + a. Validate both values are `u32` (error: `ERR_NOT_U32`). + b. Byte-swap each limb from little-endian to big-endian via `utils::swap_u32_bytes` (see [Section 5.5](#55-endianness-summary)). + c. Pack into a felt: `suffix = bswap(limb3) × 2^32 + bswap(limb4)`. + d. Verify no mod-p reduction: split the felt back via `u32split` and assert equality + with the original limbs (error: `ERR_FELT_OUT_OF_FIELD`). +3. Build `prefix` from `(limb2, limb1)` using the same `build_felt` procedure. +4. Return `[prefix, suffix]` on the stack. + +**Helper: `build_felt`** + +```text +Inputs: [lo, hi] (little-endian u32 limbs, little-endian bytes) +Outputs: [felt] +``` + +1. `u32assert2` — both inputs must be valid `u32`. +2. Byte-swap each limb: `lo_be = bswap(lo)`, `hi_be = bswap(hi)`. +3. Compute `felt = hi_be × 2^32 + lo_be`. +4. Round-trip check: `u32split(felt)` must yield `(hi_be, lo_be)`. If not, the + combined value exceeded the field modulus. + +**Helper: `utils::swap_u32_bytes`** + +```text +Inputs: [value] +Outputs: [swapped] +``` + +Reverses the byte order of a `u32`: `[b0, b1, b2, b3] → [b3, b2, b1, b0]`. + +#### 5.4.4 Ethereum Address → Field Elements (Rust) + +`EthAddressFormat::to_elements(&self) -> Vec` + +Converts the 20-byte address into a field element array for use in the Miden VM. +Each 4-byte chunk is interpreted as a **little-endian** `u32` and stored as a `Felt`. +The output order matches the big-endian limb order described in [Section 5.3](#53-masm-limb-representation). + +This is used when constructing `NoteStorage` for B2AGG notes (see [Section 3.1](#31-b2agg)) and CLAIM notes (see [Section 3.2](#32-claim)). + +### 5.5 Endianness Summary + +The conversion involves multiple levels of byte ordering: this table clarifies the different conventions used. + +| Level | Convention | Detail | +|-------|-----------|--------| +| **Limb order** | Big-endian | `address[0]` holds the most-significant 4 bytes of the 20-byte address | +| **Byte order within each limb** | Little-endian | The 4 bytes of a limb are packed as `b0 + b1×2^8 + b2×2^16 + b3×2^24` | +| **Felt packing (u64)** | Big-endian u32 pairs | `felt = hi_be × 2^32 + lo_be` where `hi_be` and `lo_be` are field elements representing the big-endian-encoded `u32` values | + +The byte swap (`swap_u32_bytes`) in the MASM `build_felt` procedure bridges between +the little-endian bytes within each limb in `NoteStorage` and the big-endian-bytes within the `u32` pairs needed to construct the prefix and suffix in the MASM `build_felt` procedure. + +### 5.6 Roundtrip Guarantee + +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. diff --git a/crates/miden-agglayer/asm/agglayer/common/eth_address.masm b/crates/miden-agglayer/asm/agglayer/common/eth_address.masm index 87882aeb9a..5001fd769d 100644 --- a/crates/miden-agglayer/asm/agglayer/common/eth_address.masm +++ b/crates/miden-agglayer/asm/agglayer/common/eth_address.masm @@ -64,7 +64,7 @@ end # HELPER PROCEDURES # ================================================================================================= -#! Builds a single felt from two u32 limbs (little-endian limb order). +#! Builds a single felt from two u32 limbs (little-endian limb order, little-endian bytes). #! Conceptually, this is packing a 64-bit word (lo + (hi << 32)) into a field element. #! This proc additionally verifies that the packed value did *not* reduce mod p by round-tripping #! through u32split and comparing the limbs. From ecb6359a6b60680c6737b3a7236f48bb681a21b6 Mon Sep 17 00:00:00 2001 From: krlosMata <44141767+krlosMata@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:41:57 +0100 Subject: [PATCH 3/4] chore(spec): fix numbering and formatting in algorithm steps (#2547) --- crates/miden-agglayer/SPEC.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 57a33b3f91..7fc10e19b0 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -632,11 +632,11 @@ Invocation: exec 1. `assertz limb0` — the most-significant limb must be zero (error: `ERR_MSB_NONZERO`). 2. Build `suffix` from `(limb4, limb3)`: - a. Validate both values are `u32` (error: `ERR_NOT_U32`). - b. Byte-swap each limb from little-endian to big-endian via `utils::swap_u32_bytes` (see [Section 5.5](#55-endianness-summary)). - c. Pack into a felt: `suffix = bswap(limb3) × 2^32 + bswap(limb4)`. - d. Verify no mod-p reduction: split the felt back via `u32split` and assert equality - with the original limbs (error: `ERR_FELT_OUT_OF_FIELD`). + - a. Validate both values are `u32` (error: `ERR_NOT_U32`). + - b. Byte-swap each limb from little-endian to big-endian via `utils::swap_u32_bytes` (see [Section 5.5](#55-endianness-summary)). + - c. Pack into a felt: `suffix = bswap(limb3) × 2^32 + bswap(limb4)`. + - d. Verify no mod-p reduction: split the felt back via `u32split` and assert equality + with the original limbs (error: `ERR_FELT_OUT_OF_FIELD 3. Build `prefix` from `(limb2, limb1)` using the same `build_felt` procedure. 4. Return `[prefix, suffix]` on the stack. From 35ae1c4f3d2c61e877c414cc86d2b2a14b4da8ea Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Fri, 13 Mar 2026 21:48:00 +0300 Subject: [PATCH 4/4] feat: Create storage helpers for `AggLayerBridge` (#2562) * feat: impl storage helpers for AggLayerBridge * refactor: add account code check * refactor: rework the procedures check * chore: move bridge and faucet to their own modules * feat: add storage helpers for the agglayer faucet * test: impl test for helpers, fix bug * chore: remove debug log * refactor: update the way constants are generated * chore: remove debug assert, update comments --- crates/miden-agglayer/build.rs | 101 +++- crates/miden-agglayer/src/bridge.rs | 382 +++++++++++++++ crates/miden-agglayer/src/faucet.rs | 440 ++++++++++++++++++ crates/miden-agglayer/src/lib.rs | 352 +------------- .../tests/agglayer/bridge_out.rs | 50 +- .../tests/agglayer/config_bridge.rs | 8 +- .../tests/agglayer/faucet_helpers.rs | 60 +++ crates/miden-testing/tests/agglayer/mod.rs | 1 + .../tests/agglayer/update_ger.rs | 29 +- 9 files changed, 989 insertions(+), 434 deletions(-) create mode 100644 crates/miden-agglayer/src/bridge.rs create mode 100644 crates/miden-agglayer/src/faucet.rs create mode 100644 crates/miden-testing/tests/agglayer/faucet_helpers.rs diff --git a/crates/miden-agglayer/build.rs b/crates/miden-agglayer/build.rs index 24f48a3091..b085e5e9b2 100644 --- a/crates/miden-agglayer/build.rs +++ b/crates/miden-agglayer/build.rs @@ -1,4 +1,5 @@ use std::env; +use std::fmt::Write; use std::path::Path; use fs_err as fs; @@ -6,7 +7,14 @@ use miden_assembly::diagnostics::{IntoDiagnostic, NamedSource, Result, WrapErr}; use miden_assembly::utils::Serializable; use miden_assembly::{Assembler, Library, Report}; use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest}; +use miden_protocol::account::{ + AccountCode, + AccountComponent, + AccountComponentMetadata, + AccountType, +}; use miden_protocol::transaction::TransactionKernel; +use miden_standards::account::auth::NoAuth; // CONSTANTS // ================================================================================================ @@ -25,6 +33,7 @@ const ASM_COMPONENTS_DIR: &str = "components"; const AGGLAYER_ERRORS_FILE: &str = "src/errors/agglayer.rs"; const AGGLAYER_ERRORS_ARRAY_NAME: &str = "AGGLAYER_ERRORS"; +const AGGLAYER_GLOBAL_CONSTANTS_FILE_NAME: &str = "agglayer_constants.rs"; // PRE-PROCESSING // ================================================================================================ @@ -62,8 +71,8 @@ fn main() -> Result<()> { let mut assembler = TransactionKernel::assembler(); assembler.link_static_library(agglayer_lib)?; - // compile account components (thin wrappers per component) - compile_account_components( + // compile account components (thin wrappers per component) and return their libraries + let component_libraries = compile_account_components( &source_dir.join(ASM_COMPONENTS_DIR), &target_dir.join(ASM_COMPONENTS_DIR), assembler.clone(), @@ -76,6 +85,10 @@ fn main() -> Result<()> { assembler.clone(), )?; + // generate agglayer specific constants + let constants_out_path = Path::new(&build_dir).join(AGGLAYER_GLOBAL_CONSTANTS_FILE_NAME); + generate_agglayer_constants(constants_out_path, component_libraries)?; + generate_error_constants(&source_dir)?; Ok(()) @@ -149,8 +162,9 @@ fn compile_note_scripts( // COMPILE ACCOUNT COMPONENTS // ================================================================================================ -/// Compiles the account components in `source_dir` into MASL libraries and stores the compiled -/// files in `target_dir`. +/// Compiles the account components in `source_dir` into MASL libraries, stores the compiled +/// files in `target_dir`, and returns a vector of compiled component libraries along with their +/// names. /// /// Each `.masm` file in the components directory is a thin wrapper that re-exports specific /// procedures from the main agglayer library. This ensures each component (bridge, faucet) @@ -162,11 +176,13 @@ fn compile_account_components( source_dir: &Path, target_dir: &Path, assembler: Assembler, -) -> Result<()> { +) -> Result> { if !target_dir.exists() { fs::create_dir_all(target_dir).unwrap(); } + let mut component_libraries = Vec::new(); + for masm_file_path in shared::get_masm_files(source_dir).unwrap() { let component_name = masm_file_path .file_stem() @@ -186,10 +202,81 @@ fn compile_account_components( .expect("library assembly should succeed"); let component_file_path = - target_dir.join(component_name).with_extension(Library::LIBRARY_EXTENSION); - component_library.write_to_file(component_file_path).into_diagnostic()?; + target_dir.join(&component_name).with_extension(Library::LIBRARY_EXTENSION); + component_library.write_to_file(&component_file_path).into_diagnostic()?; + + component_libraries.push((component_name, component_library)); } + Ok(component_libraries) +} + +// GENERATE AGGLAYER CONSTANTS +// ================================================================================================ + +/// Generates a Rust file containing AggLayer specific constants. +/// +/// At the moment, this file contains the following constants: +/// - AggLayer Bridge code commitment. +/// - AggLayer Faucet code commitment. +fn generate_agglayer_constants( + target_file: impl AsRef, + component_libraries: Vec<(String, Library)>, +) -> Result<()> { + let mut file_contents = String::new(); + + writeln!( + file_contents, + "// This file is generated by build.rs, do not modify manually.\n" + ) + .unwrap(); + + writeln!( + file_contents, + "// AGGLAYER CONSTANTS +// ================================================================================================ +" + ) + .unwrap(); + + // Create a dummy metadata to be able to create components. We only interested in the resulting + // code commitment, so it doesn't matter what does this metadata holds. + let dummy_metadata = AccountComponentMetadata::new("dummy").with_supports_all_types(); + + // iterate over the AggLayer Bridge and AggLayer Faucet libraries + for (lib_name, content_library) in component_libraries { + let agglayer_component = + AccountComponent::new(content_library, vec![], dummy_metadata.clone()).unwrap(); + + // use `AccountCode` to merge codes of agglayer and authentication components + let account_code = AccountCode::from_components( + &[AccountComponent::from(NoAuth), agglayer_component], + AccountType::FungibleFaucet, + ) + .expect("account code creation failed"); + + let code_commitment = account_code.commitment(); + + writeln!( + file_contents, + "pub const {}_CODE_COMMITMENT: Word = Word::new([ + Felt::new({}), + Felt::new({}), + Felt::new({}), + Felt::new({}), +]);", + lib_name.to_uppercase(), + code_commitment[0], + code_commitment[1], + code_commitment[2], + code_commitment[3], + ) + .unwrap(); + } + + // write the resulting constants to the target directory + shared::write_if_changed(target_file, file_contents.as_bytes())?; + Ok(()) } diff --git a/crates/miden-agglayer/src/bridge.rs b/crates/miden-agglayer/src/bridge.rs new file mode 100644 index 0000000000..8dbe916cbe --- /dev/null +++ b/crates/miden-agglayer/src/bridge.rs @@ -0,0 +1,382 @@ +extern crate alloc; + +use alloc::vec; +use alloc::vec::Vec; + +use miden_core::{Felt, FieldElement, ONE, Word, ZERO}; +use miden_protocol::account::component::AccountComponentMetadata; +use miden_protocol::account::{Account, AccountComponent, AccountId, StorageSlot, StorageSlotName}; +use miden_protocol::crypto::hash::rpo::Rpo256; +use miden_utils_sync::LazyLock; +use thiserror::Error; + +use super::agglayer_bridge_component_library; +pub use crate::{ + B2AggNote, + ClaimNoteStorage, + ConfigAggBridgeNote, + EthAddressFormat, + EthAmount, + EthAmountError, + ExitRoot, + GlobalIndex, + GlobalIndexError, + LeafData, + MetadataHash, + ProofData, + SmtNode, + UpdateGerNote, + create_claim_note, +}; + +// CONSTANTS +// ================================================================================================ +// Include the generated agglayer constants +include!(concat!(env!("OUT_DIR"), "/agglayer_constants.rs")); + +// AGGLAYER BRIDGE STRUCT +// ================================================================================================ + +static GER_MAP_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::bridge::ger") + .expect("bridge storage slot name should be valid") +}); +static LET_FRONTIER_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::let").expect("LET storage slot name should be valid") +}); +static LET_ROOT_LO_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::let::root_lo") + .expect("LET root_lo storage slot name should be valid") +}); +static LET_ROOT_HI_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::let::root_hi") + .expect("LET root_hi storage slot name should be valid") +}); +static LET_NUM_LEAVES_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::let::num_leaves") + .expect("LET num_leaves storage slot name should be valid") +}); +static FAUCET_REGISTRY_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::bridge::faucet_registry") + .expect("faucet registry storage slot name should be valid") +}); +static BRIDGE_ADMIN_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::bridge::admin") + .expect("bridge admin storage slot name should be valid") +}); +static GER_MANAGER_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::bridge::ger_manager") + .expect("GER manager storage slot name should be valid") +}); + +/// An [`AccountComponent`] implementing the AggLayer Bridge. +/// +/// It reexports the procedures from `miden::agglayer::bridge`. When linking against this +/// component, the `agglayer` library must be available to the assembler. +/// The procedures of this component are: +/// - `register_faucet`, which registers a faucet in the bridge. +/// - `update_ger`, which injects a new GER into the storage map. +/// - `verify_leaf_bridge`, which verifies a deposit leaf against one of the stored GERs. +/// - `bridge_out`, which bridges an asset out of Miden to the destination network. +/// +/// ## Storage Layout +/// +/// - [`Self::ger_map_slot_name`]: Stores the GERs. +/// - [`Self::let_frontier_slot_name`]: Stores the Local Exit Tree (LET) frontier. +/// - [`Self::ler_lo_slot_name`]: Stores the lower 32 bits of the LET root. +/// - [`Self::ler_hi_slot_name`]: Stores the upper 32 bits of the LET root. +/// - [`Self::let_num_leaves_slot_name`]: Stores the number of leaves in the LET frontier. +/// - [`Self::faucet_registry_slot_name`]: Stores the faucet registry map. +/// - [`Self::bridge_admin_slot_name`]: Stores the bridge admin account ID. +/// - [`Self::ger_manager_slot_name`]: Stores the GER manager account ID. +/// +/// The bridge starts with an empty faucet registry; faucets are registered at runtime via +/// CONFIG_AGG_BRIDGE notes. +#[derive(Debug, Clone)] +pub struct AggLayerBridge { + bridge_admin_id: AccountId, + ger_manager_id: AccountId, +} + +impl AggLayerBridge { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + const REGISTERED_GER_MAP_VALUE: Word = Word::new([ONE, ZERO, ZERO, ZERO]); + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new AggLayer bridge component with the standard configuration. + pub fn new(bridge_admin_id: AccountId, ger_manager_id: AccountId) -> Self { + Self { bridge_admin_id, ger_manager_id } + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Storage slot name for the GERs map. + pub fn ger_map_slot_name() -> &'static StorageSlotName { + &GER_MAP_SLOT_NAME + } + + /// Storage slot name for the Local Exit Tree (LET) frontier. + pub fn let_frontier_slot_name() -> &'static StorageSlotName { + &LET_FRONTIER_SLOT_NAME + } + + /// Storage slot name for the lower 32 bits of the LET root. + pub fn ler_lo_slot_name() -> &'static StorageSlotName { + &LET_ROOT_LO_SLOT_NAME + } + + /// Storage slot name for the upper 32 bits of the LET root. + pub fn ler_hi_slot_name() -> &'static StorageSlotName { + &LET_ROOT_HI_SLOT_NAME + } + + /// Storage slot name for the number of leaves in the LET frontier. + pub fn let_num_leaves_slot_name() -> &'static StorageSlotName { + &LET_NUM_LEAVES_SLOT_NAME + } + + /// Storage slot name for the faucet registry map. + pub fn faucet_registry_slot_name() -> &'static StorageSlotName { + &FAUCET_REGISTRY_SLOT_NAME + } + + /// Storage slot name for the bridge admin account ID. + pub fn bridge_admin_slot_name() -> &'static StorageSlotName { + &BRIDGE_ADMIN_SLOT_NAME + } + + /// Storage slot name for the GER manager account ID. + pub fn ger_manager_slot_name() -> &'static StorageSlotName { + &GER_MANAGER_SLOT_NAME + } + + /// Returns a boolean indicating whether the provided GER is present in storage of the provided + /// bridge account. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided account is not an [`AggLayerBridge`] account. + pub fn is_ger_registered( + ger: ExitRoot, + bridge_account: Account, + ) -> Result { + // check that the provided account is a bridge account + Self::assert_bridge_account(&bridge_account)?; + + // Compute the expected GER hash: rpo256::merge(GER_UPPER, GER_LOWER) + let mut ger_lower: [Felt; 4] = ger.to_elements()[0..4].try_into().unwrap(); + let mut ger_upper: [Felt; 4] = ger.to_elements()[4..8].try_into().unwrap(); + // Elements are reversed: rpo256::merge treats stack as if loaded BE from memory + // The following will produce matching hashes: + // Rust + // Hasher::merge(&[a, b, c, d], &[e, f, g, h]) + // MASM + // rpo256::merge(h, g, f, e, d, c, b, a) + ger_lower.reverse(); + ger_upper.reverse(); + let ger_hash = Rpo256::merge(&[ger_upper.into(), ger_lower.into()]); + + // Get the value stored by the GER hash. If this GER was registered, the value would be + // equal to [1, 0, 0, 0] + let stored_value = bridge_account + .storage() + .get_map_item(AggLayerBridge::ger_map_slot_name(), ger_hash) + .expect("provided account should have AggLayer Bridge specific storage slots"); + + if stored_value == Self::REGISTERED_GER_MAP_VALUE { + Ok(true) + } else { + Ok(false) + } + } + + /// Reads the Local Exit Root (double-word) from the bridge account's storage. + /// + /// The Local Exit Root is stored in two dedicated value slots: + /// - [`AggLayerBridge::ler_lo_slot_name`] — low word of the root + /// - [`AggLayerBridge::ler_hi_slot_name`] — high word of the root + /// + /// Returns the 256-bit root as 8 `Felt`s: first the 4 elements of `root_lo` (in + /// reverse of their storage order), followed by the 4 elements of `root_hi` (also in + /// reverse of their storage order). For an empty/uninitialized tree, all elements are + /// zeros. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided account is not an [`AggLayerBridge`] account. + pub fn read_local_exit_root(account: &Account) -> Result, AgglayerBridgeError> { + // check that the provided account is a bridge account + Self::assert_bridge_account(account)?; + + let root_lo_slot = AggLayerBridge::ler_lo_slot_name(); + let root_hi_slot = AggLayerBridge::ler_hi_slot_name(); + + let root_lo = account + .storage() + .get_item(root_lo_slot) + .expect("should be able to read LET root lo"); + let root_hi = account + .storage() + .get_item(root_hi_slot) + .expect("should be able to read LET root hi"); + + let mut root = Vec::with_capacity(8); + root.extend(root_lo.to_vec().into_iter().rev()); + root.extend(root_hi.to_vec().into_iter().rev()); + + Ok(root) + } + + /// Returns the number of leaves in the Local Exit Tree (LET) frontier. + pub fn read_let_num_leaves(account: &Account) -> u64 { + let num_leaves_slot = AggLayerBridge::let_num_leaves_slot_name(); + let value = account + .storage() + .get_item(num_leaves_slot) + .expect("should be able to read LET num leaves"); + value.to_vec()[0].as_int() + } + + // HELPER FUNCTIONS + // -------------------------------------------------------------------------------------------- + + /// Checks that the provided account is an [`AggLayerBridge`] account. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided account does not have all AggLayer Bridge specific storage slots. + /// - the code commitment of the provided account does not match the code commitment of the + /// [`AggLayerBridge`]. + fn assert_bridge_account(account: &Account) -> Result<(), AgglayerBridgeError> { + // check that the storage slots are as expected + Self::assert_storage_slots(account)?; + + // check that the code commitment matches the code commitment of the bridge account + Self::assert_code_commitment(account)?; + + Ok(()) + } + + /// Checks that the provided account has all storage slots required for the [`AggLayerBridge`]. + /// + /// # Errors + /// + /// Returns an error if: + /// - provided account does not have all AggLayer Bridge specific storage slots. + fn assert_storage_slots(account: &Account) -> Result<(), AgglayerBridgeError> { + // get the storage slot names of the provided account + let account_storage_slot_names: Vec<&StorageSlotName> = account + .storage() + .slots() + .iter() + .map(|storage_slot| storage_slot.name()) + .collect::>(); + + // check that all bridge specific storage slots are presented in the provided account + let are_slots_present = Self::slot_names() + .iter() + .all(|slot_name| account_storage_slot_names.contains(slot_name)); + if !are_slots_present { + return Err(AgglayerBridgeError::StorageSlotsMismatch); + } + + Ok(()) + } + + /// Checks that the code commitment of the provided account matches the code commitment of the + /// [`AggLayerBridge`]. + /// + /// # Errors + /// + /// Returns an error if: + /// - the code commitment of the provided account does not match the code commitment of the + /// [`AggLayerBridge`]. + fn assert_code_commitment(account: &Account) -> Result<(), AgglayerBridgeError> { + if BRIDGE_CODE_COMMITMENT != account.code().commitment() { + return Err(AgglayerBridgeError::CodeCommitmentMismatch); + } + + Ok(()) + } + + /// Returns a vector of all [`AggLayerBridge`] storage slot names. + fn slot_names() -> Vec<&'static StorageSlotName> { + vec![ + &*GER_MAP_SLOT_NAME, + &*LET_FRONTIER_SLOT_NAME, + &*LET_ROOT_LO_SLOT_NAME, + &*LET_ROOT_HI_SLOT_NAME, + &*LET_NUM_LEAVES_SLOT_NAME, + &*FAUCET_REGISTRY_SLOT_NAME, + &*BRIDGE_ADMIN_SLOT_NAME, + &*GER_MANAGER_SLOT_NAME, + ] + } +} + +impl From for AccountComponent { + fn from(bridge: AggLayerBridge) -> Self { + let bridge_admin_word = Word::new([ + Felt::ZERO, + Felt::ZERO, + bridge.bridge_admin_id.suffix(), + bridge.bridge_admin_id.prefix().as_felt(), + ]); + let ger_manager_word = Word::new([ + Felt::ZERO, + Felt::ZERO, + bridge.ger_manager_id.suffix(), + bridge.ger_manager_id.prefix().as_felt(), + ]); + + let bridge_storage_slots = vec![ + StorageSlot::with_empty_map(GER_MAP_SLOT_NAME.clone()), + StorageSlot::with_empty_map(LET_FRONTIER_SLOT_NAME.clone()), + StorageSlot::with_value(LET_ROOT_LO_SLOT_NAME.clone(), Word::empty()), + StorageSlot::with_value(LET_ROOT_HI_SLOT_NAME.clone(), Word::empty()), + StorageSlot::with_value(LET_NUM_LEAVES_SLOT_NAME.clone(), Word::empty()), + StorageSlot::with_empty_map(FAUCET_REGISTRY_SLOT_NAME.clone()), + StorageSlot::with_value(BRIDGE_ADMIN_SLOT_NAME.clone(), bridge_admin_word), + StorageSlot::with_value(GER_MANAGER_SLOT_NAME.clone(), ger_manager_word), + ]; + bridge_component(bridge_storage_slots) + } +} + +// AGGLAYER BRIDGE ERROR +// ================================================================================================ + +/// AggLayer Bridge related errors. +#[derive(Debug, Error)] +pub enum AgglayerBridgeError { + #[error( + "provided account does not have storage slots required for the AggLayer Bridge account" + )] + StorageSlotsMismatch, + #[error( + "the code commitment of the provided account does not match the code commitment of the AggLayer Bridge account" + )] + CodeCommitmentMismatch, +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Creates an AggLayer Bridge component with the specified storage slots. +fn bridge_component(storage_slots: Vec) -> AccountComponent { + let library = agglayer_bridge_component_library(); + let metadata = AccountComponentMetadata::new("agglayer::bridge") + .with_description("Bridge component for AggLayer") + .with_supports_all_types(); + + AccountComponent::new(library, storage_slots, metadata) + .expect("bridge component should satisfy the requirements of a valid account component") +} diff --git a/crates/miden-agglayer/src/faucet.rs b/crates/miden-agglayer/src/faucet.rs new file mode 100644 index 0000000000..43e1f02b22 --- /dev/null +++ b/crates/miden-agglayer/src/faucet.rs @@ -0,0 +1,440 @@ +extern crate alloc; + +use alloc::vec; +use alloc::vec::Vec; + +use miden_core::{Felt, FieldElement, Word}; +use miden_protocol::account::component::AccountComponentMetadata; +use miden_protocol::account::{ + Account, + AccountComponent, + AccountId, + AccountType, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::asset::TokenSymbol; +use miden_protocol::errors::AccountIdError; +use miden_standards::account::faucets::{FungibleFaucetError, TokenMetadata}; +use miden_utils_sync::LazyLock; +use thiserror::Error; + +use super::agglayer_faucet_component_library; +pub use crate::{ + AggLayerBridge, + B2AggNote, + ClaimNoteStorage, + ConfigAggBridgeNote, + EthAddressFormat, + EthAmount, + EthAmountError, + ExitRoot, + GlobalIndex, + GlobalIndexError, + LeafData, + MetadataHash, + ProofData, + SmtNode, + UpdateGerNote, + create_claim_note, +}; + +// CONSTANTS +// ================================================================================================ +// Include the generated agglayer constants +include!(concat!(env!("OUT_DIR"), "/agglayer_constants.rs")); + +// AGGLAYER FAUCET STRUCT +// ================================================================================================ + +static AGGLAYER_FAUCET_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::faucet") + .expect("agglayer faucet storage slot name should be valid") +}); +static CONVERSION_INFO_1_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::faucet::conversion_info_1") + .expect("conversion info 1 storage slot name should be valid") +}); +static CONVERSION_INFO_2_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::faucet::conversion_info_2") + .expect("conversion info 2 storage slot name should be valid") +}); + +/// An [`AccountComponent`] implementing the AggLayer Faucet. +/// +/// It reexports the procedures from `miden::agglayer::faucet`. When linking against this +/// component, the `agglayer` library must be available to the assembler. +/// The procedures of this component are: +/// - `claim`, which validates a CLAIM note against one of the stored GERs in the bridge. +/// - `asset_to_origin_asset`, which converts an asset to the origin asset (used in FPI from +/// bridge). +/// - `burn`, which burns an asset. +/// +/// ## Storage Layout +/// +/// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. +/// - [`Self::bridge_account_id_slot`]: Stores the AggLayer bridge account ID. +/// - [`Self::conversion_info_1_slot`]: Stores the first 4 felts of the origin token address. +/// - [`Self::conversion_info_2_slot`]: Stores the remaining 5th felt of the origin token address + +/// origin network + scale. +#[derive(Debug, Clone)] +pub struct AggLayerFaucet { + metadata: TokenMetadata, + bridge_account_id: AccountId, + origin_token_address: EthAddressFormat, + origin_network: u32, + scale: u8, +} + +impl AggLayerFaucet { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new AggLayer faucet component from the given configuration. + /// + /// # Errors + /// Returns an error if: + /// - The decimals parameter exceeds maximum value of [`TokenMetadata::MAX_DECIMALS`]. + /// - The max supply exceeds maximum possible amount for a fungible asset. + /// - The token supply exceeds the max supply. + pub fn new( + symbol: TokenSymbol, + decimals: u8, + max_supply: Felt, + token_supply: Felt, + bridge_account_id: AccountId, + origin_token_address: EthAddressFormat, + origin_network: u32, + scale: u8, + ) -> Result { + let metadata = TokenMetadata::with_supply(symbol, decimals, max_supply, token_supply)?; + Ok(Self { + metadata, + bridge_account_id, + origin_token_address, + origin_network, + scale, + }) + } + + /// Sets the token supply for an existing faucet (e.g. for testing scenarios). + /// + /// # Errors + /// Returns an error if the token supply exceeds the max supply. + pub fn with_token_supply(mut self, token_supply: Felt) -> Result { + self.metadata = self.metadata.with_token_supply(token_supply)?; + Ok(self) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Storage slot name for [`TokenMetadata`]. + pub fn metadata_slot() -> &'static StorageSlotName { + TokenMetadata::metadata_slot() + } + + /// Storage slot name for the AggLayer bridge account ID. + pub fn bridge_account_id_slot() -> &'static StorageSlotName { + &AGGLAYER_FAUCET_SLOT_NAME + } + + /// Storage slot name for the first 4 felts of the origin token address. + pub fn conversion_info_1_slot() -> &'static StorageSlotName { + &CONVERSION_INFO_1_SLOT_NAME + } + + /// Storage slot name for the 5th felt of the origin token address, origin network, and scale. + pub fn conversion_info_2_slot() -> &'static StorageSlotName { + &CONVERSION_INFO_2_SLOT_NAME + } + + /// Extracts the token metadata from the corresponding storage slot of the provided account. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided account is not an [`AggLayerFaucet`] account. + pub fn metadata(faucet_account: &Account) -> Result { + // check that the provided account is a faucet account + Self::assert_faucet_account(faucet_account)?; + + let metadata_word = faucet_account + .storage() + .get_item(TokenMetadata::metadata_slot()) + .expect("should be able to read metadata slot"); + TokenMetadata::try_from(metadata_word).map_err(AgglayerFaucetError::FungibleFaucetError) + } + + /// Extracts the bridge account ID from the corresponding storage slot of the provided account. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided account is not an [`AggLayerFaucet`] account. + pub fn bridge_account_id(faucet_account: &Account) -> Result { + // check that the provided account is a faucet account + Self::assert_faucet_account(faucet_account)?; + + let bridge_id_word = faucet_account + .storage() + .get_item(&AGGLAYER_FAUCET_SLOT_NAME) + .expect("should be able to read account ID slot"); + AccountId::try_from([bridge_id_word[3], bridge_id_word[2]]) + .map_err(AgglayerFaucetError::AccountIdError) + } + + /// Extracts the origin token address from the corresponding storage slot of the provided + /// account. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided account is not an [`AggLayerFaucet`] account. + pub fn origin_token_address( + faucet_account: &Account, + ) -> Result { + // check that the provided account is a faucet account + Self::assert_faucet_account(faucet_account)?; + + let conversion_info_1 = faucet_account + .storage() + .get_item(&CONVERSION_INFO_1_SLOT_NAME) + .expect("should be able to read the first conversion info slot"); + + let conversion_info_2 = faucet_account + .storage() + .get_item(&CONVERSION_INFO_2_SLOT_NAME) + .expect("should be able to read the second conversion info slot"); + + let addr_bytes_vec = conversion_info_1 + .iter() + .chain([&conversion_info_2[0]]) + .flat_map(|felt| (felt.as_int() as u32).to_le_bytes()) + .collect::>(); + + Ok(EthAddressFormat::new( + addr_bytes_vec + .try_into() + .expect("origin token addr vector should consist of exactly 20 bytes"), + )) + } + + /// Extracts the origin network ID in form of the u32 from the corresponding storage slot of the + /// provided account. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided account is not an [`AggLayerFaucet`] account. + pub fn origin_network(faucet_account: &Account) -> Result { + // check that the provided account is a faucet account + Self::assert_faucet_account(faucet_account)?; + + let conversion_info_2 = faucet_account + .storage() + .get_item(&CONVERSION_INFO_2_SLOT_NAME) + .expect("should be able to read the second conversion info slot"); + + Ok(conversion_info_2[1].try_into().expect("origin network ID should fit into u32")) + } + + /// Extracts the scaling factor in form of the u8 from the corresponding storage slot of the + /// provided account. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided account is not an [`AggLayerFaucet`] account. + pub fn scale(faucet_account: &Account) -> Result { + // check that the provided account is a faucet account + Self::assert_faucet_account(faucet_account)?; + + let conversion_info_2 = faucet_account + .storage() + .get_item(&CONVERSION_INFO_2_SLOT_NAME) + .expect("should be able to read the second conversion info slot"); + + Ok(conversion_info_2[2].try_into().expect("scaling factor should fit into u8")) + } + + // HELPER FUNCTIONS + // -------------------------------------------------------------------------------------------- + + /// Checks that the provided account is an [`AggLayerFaucet`] account. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided account does not have all AggLayer Faucet specific storage slots. + /// - the provided account does not have all AggLayer Faucet specific procedures. + fn assert_faucet_account(account: &Account) -> Result<(), AgglayerFaucetError> { + // check that the storage slots are as expected + Self::assert_storage_slots(account)?; + + // check that the procedure roots are as expected + Self::assert_code_commitment(account)?; + + Ok(()) + } + + /// Checks that the provided account has all storage slots required for the [`AggLayerFaucet`]. + /// + /// # Errors + /// + /// Returns an error if: + /// - provided account does not have all AggLayer Faucet specific storage slots). + fn assert_storage_slots(account: &Account) -> Result<(), AgglayerFaucetError> { + // get the storage slot names of the provided account + let account_storage_slot_names: Vec<&StorageSlotName> = account + .storage() + .slots() + .iter() + .map(|storage_slot| storage_slot.name()) + .collect::>(); + + // check that all bridge specific storage slots are presented in the provided account + let are_slots_present = Self::slot_names() + .iter() + .all(|slot_name| account_storage_slot_names.contains(slot_name)); + if !are_slots_present { + return Err(AgglayerFaucetError::StorageSlotsMismatch); + } + + Ok(()) + } + + /// Checks that the code commitment of the provided account matches the code commitment of the + /// [`AggLayerFaucet`]. + /// + /// # Errors + /// + /// Returns an error if: + /// - the code commitment of the provided account does not match the code commitment of the + /// [`AggLayerFaucet`]. + fn assert_code_commitment(account: &Account) -> Result<(), AgglayerFaucetError> { + if FAUCET_CODE_COMMITMENT != account.code().commitment() { + return Err(AgglayerFaucetError::CodeCommitmentMismatch); + } + + Ok(()) + } + + /// Returns a vector of all [`AggLayerFaucet`] storage slot names. + fn slot_names() -> Vec<&'static StorageSlotName> { + vec![ + &*AGGLAYER_FAUCET_SLOT_NAME, + &*CONVERSION_INFO_1_SLOT_NAME, + &*CONVERSION_INFO_2_SLOT_NAME, + ] + } +} + +impl From for AccountComponent { + fn from(faucet: AggLayerFaucet) -> Self { + let metadata_slot = StorageSlot::from(faucet.metadata); + + let bridge_account_id_word = Word::new([ + Felt::ZERO, + Felt::ZERO, + faucet.bridge_account_id.suffix(), + faucet.bridge_account_id.prefix().as_felt(), + ]); + let bridge_slot = + StorageSlot::with_value(AGGLAYER_FAUCET_SLOT_NAME.clone(), bridge_account_id_word); + + let (conversion_slot1_word, conversion_slot2_word) = agglayer_faucet_conversion_slots( + &faucet.origin_token_address, + faucet.origin_network, + faucet.scale, + ); + let conversion_slot1 = + StorageSlot::with_value(CONVERSION_INFO_1_SLOT_NAME.clone(), conversion_slot1_word); + let conversion_slot2 = + StorageSlot::with_value(CONVERSION_INFO_2_SLOT_NAME.clone(), conversion_slot2_word); + + let agglayer_storage_slots = + vec![metadata_slot, bridge_slot, conversion_slot1, conversion_slot2]; + agglayer_faucet_component(agglayer_storage_slots) + } +} + +// AGGLAYER FAUCET ERROR +// ================================================================================================ + +/// AggLayer Faucet related errors. +#[derive(Debug, Error)] +pub enum AgglayerFaucetError { + #[error( + "provided account does not have storage slots required for the AggLayer Faucet account" + )] + StorageSlotsMismatch, + #[error("provided account does not have procedures required for the AggLayer Faucet account")] + CodeCommitmentMismatch, + #[error("fungible faucet error")] + FungibleFaucetError(#[source] FungibleFaucetError), + #[error("account ID error")] + AccountIdError(#[source] AccountIdError), +} + +// FAUCET REGISTRY HELPERS +// ================================================================================================ + +/// Creates a faucet registry map key from a faucet account ID. +/// +/// The key format is `[0, 0, faucet_id_suffix, faucet_id_prefix]`. +pub fn faucet_registry_key(faucet_id: AccountId) -> Word { + Word::new([Felt::ZERO, Felt::ZERO, faucet_id.suffix(), faucet_id.prefix().as_felt()]) +} + +// FAUCET CONVERSION STORAGE HELPERS +// ================================================================================================ + +/// Builds the two storage slot values for faucet conversion metadata. +/// +/// The conversion metadata is stored in two value storage slots: +/// - Slot 1 (`miden::agglayer::faucet::conversion_info_1`): `[addr0, addr1, addr2, addr3]` — first +/// 4 felts of the origin token address (5 × u32 limbs). +/// - Slot 2 (`miden::agglayer::faucet::conversion_info_2`): `[addr4, origin_network, scale, 0]` — +/// remaining address felt + origin network + scale factor. +/// +/// # Parameters +/// - `origin_token_address`: The EVM token address in Ethereum format +/// - `origin_network`: The origin network/chain ID +/// - `scale`: The decimal scaling factor (exponent for 10^scale) +/// +/// # Returns +/// A tuple of two `Word` values representing the two storage slot contents. +fn agglayer_faucet_conversion_slots( + origin_token_address: &EthAddressFormat, + origin_network: u32, + scale: u8, +) -> (Word, Word) { + let addr_elements = origin_token_address.to_elements(); + + let slot1 = Word::new([addr_elements[0], addr_elements[1], addr_elements[2], addr_elements[3]]); + + let slot2 = + Word::new([addr_elements[4], Felt::from(origin_network), Felt::from(scale), Felt::ZERO]); + + (slot1, slot2) +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Creates an Agglayer Faucet component with the specified storage slots. +/// +/// This component combines network faucet functionality with bridge validation +/// via Foreign Procedure Invocation (FPI). It provides a "claim" procedure that +/// validates CLAIM notes against a bridge MMR account before minting assets. +fn agglayer_faucet_component(storage_slots: Vec) -> AccountComponent { + let library = agglayer_faucet_component_library(); + let metadata = AccountComponentMetadata::new("agglayer::faucet") + .with_description("AggLayer faucet component with bridge validation") + .with_supported_type(AccountType::FungibleFaucet); + + AccountComponent::new(library, storage_slots, metadata).expect( + "agglayer_faucet component should satisfy the requirements of a valid account component", + ) +} diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 21070e557b..dcdf915114 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -2,13 +2,9 @@ extern crate alloc; -use alloc::vec; -use alloc::vec::Vec; - use miden_assembly::Library; use miden_assembly::utils::Deserializable; use miden_core::{Felt, FieldElement, Program, Word}; -use miden_protocol::account::component::AccountComponentMetadata; use miden_protocol::account::{ Account, AccountBuilder, @@ -16,24 +12,24 @@ use miden_protocol::account::{ AccountId, AccountStorageMode, AccountType, - StorageSlot, - StorageSlotName, }; use miden_protocol::asset::TokenSymbol; use miden_protocol::note::NoteScript; use miden_standards::account::auth::NoAuth; -use miden_standards::account::faucets::{FungibleFaucetError, TokenMetadata}; use miden_utils_sync::LazyLock; pub mod b2agg_note; +pub mod bridge; pub mod claim_note; pub mod config_note; 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; pub use claim_note::{ClaimNoteStorage, ExitRoot, LeafData, ProofData, SmtNode, create_claim_note}; pub use config_note::ConfigAggBridgeNote; pub use eth_types::{ @@ -44,6 +40,7 @@ pub use eth_types::{ GlobalIndexError, MetadataHash, }; +pub use faucet::AggLayerFaucet; pub use update_ger_note::UpdateGerNote; // AGGLAYER NOTE SCRIPTS @@ -94,347 +91,6 @@ fn agglayer_faucet_component_library() -> Library { FAUCET_COMPONENT_LIBRARY.clone() } -/// Creates an AggLayer Bridge component with the specified storage slots. -fn bridge_component(storage_slots: Vec) -> AccountComponent { - let library = agglayer_bridge_component_library(); - let metadata = AccountComponentMetadata::new("agglayer::bridge") - .with_description("Bridge component for AggLayer") - .with_supports_all_types(); - - AccountComponent::new(library, storage_slots, metadata) - .expect("bridge component should satisfy the requirements of a valid account component") -} - -// AGGLAYER BRIDGE STRUCT -// ================================================================================================ - -static GER_MAP_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::agglayer::bridge::ger") - .expect("bridge storage slot name should be valid") -}); -static LET_FRONTIER_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::agglayer::let").expect("LET storage slot name should be valid") -}); -static LET_ROOT_LO_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::agglayer::let::root_lo") - .expect("LET root_lo storage slot name should be valid") -}); -static LET_ROOT_HI_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::agglayer::let::root_hi") - .expect("LET root_hi storage slot name should be valid") -}); -static LET_NUM_LEAVES_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::agglayer::let::num_leaves") - .expect("LET num_leaves storage slot name should be valid") -}); -static FAUCET_REGISTRY_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::agglayer::bridge::faucet_registry") - .expect("faucet registry storage slot name should be valid") -}); -static BRIDGE_ADMIN_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::agglayer::bridge::admin") - .expect("bridge admin storage slot name should be valid") -}); -static GER_MANAGER_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::agglayer::bridge::ger_manager") - .expect("GER manager storage slot name should be valid") -}); - -/// An [`AccountComponent`] implementing the AggLayer Bridge. -/// -/// It reexports the procedures from `miden::agglayer::bridge`. When linking against this -/// component, the `agglayer` library must be available to the assembler. -/// The procedures of this component are: -/// - `assert_sender_is_bridge_admin`, which validates CONFIG note senders. -/// - `assert_sender_is_ger_manager`, which validates UPDATE_GER note senders. -/// - `register_faucet`, which registers a faucet in the bridge. -/// - `update_ger`, which injects a new GER into the storage map. -/// - `verify_leaf_bridge`, which verifies a deposit leaf against one of the stored GERs. -/// - `bridge_out`, which bridges an asset out of Miden to the destination network. -/// -/// ## Storage Layout -/// -/// - [`Self::ger_map_slot_name`]: Stores the GERs. -/// - [`Self::let_frontier_slot_name`]: Stores the Local Exit Tree (LET) frontier. -/// - [`Self::ler_lo_slot_name`]: Stores the lower 32 bits of the LET root. -/// - [`Self::ler_hi_slot_name`]: Stores the upper 32 bits of the LET root. -/// - [`Self::let_num_leaves_slot_name`]: Stores the number of leaves in the LET frontier. -/// - [`Self::faucet_registry_slot_name`]: Stores the faucet registry map. -/// - [`Self::bridge_admin_slot_name`]: Stores the bridge admin account ID. -/// - [`Self::ger_manager_slot_name`]: Stores the GER manager account ID. -/// -/// The bridge starts with an empty faucet registry; faucets are registered at runtime via -/// CONFIG_AGG_BRIDGE notes. -#[derive(Debug, Clone)] -pub struct AggLayerBridge { - bridge_admin_id: AccountId, - ger_manager_id: AccountId, -} - -impl AggLayerBridge { - /// Creates a new AggLayer bridge component with the standard configuration. - pub fn new(bridge_admin_id: AccountId, ger_manager_id: AccountId) -> Self { - Self { bridge_admin_id, ger_manager_id } - } - - /// Storage slot name for the GERs map. - pub fn ger_map_slot_name() -> &'static StorageSlotName { - &GER_MAP_SLOT_NAME - } - - /// Storage slot name for the Local Exit Tree (LET) frontier. - pub fn let_frontier_slot_name() -> &'static StorageSlotName { - &LET_FRONTIER_SLOT_NAME - } - - /// Storage slot name for the lower 32 bits of the LET root. - pub fn ler_lo_slot_name() -> &'static StorageSlotName { - &LET_ROOT_LO_SLOT_NAME - } - - /// Storage slot name for the upper 32 bits of the LET root. - pub fn ler_hi_slot_name() -> &'static StorageSlotName { - &LET_ROOT_HI_SLOT_NAME - } - - /// Storage slot name for the number of leaves in the LET frontier. - pub fn let_num_leaves_slot_name() -> &'static StorageSlotName { - &LET_NUM_LEAVES_SLOT_NAME - } - - /// Storage slot name for the faucet registry map. - pub fn faucet_registry_slot_name() -> &'static StorageSlotName { - &FAUCET_REGISTRY_SLOT_NAME - } - - /// Storage slot name for the bridge admin account ID. - pub fn bridge_admin_slot_name() -> &'static StorageSlotName { - &BRIDGE_ADMIN_SLOT_NAME - } - - /// Storage slot name for the GER manager account ID. - pub fn ger_manager_slot_name() -> &'static StorageSlotName { - &GER_MANAGER_SLOT_NAME - } -} - -impl From for AccountComponent { - fn from(bridge: AggLayerBridge) -> Self { - let bridge_admin_word = Word::new([ - Felt::ZERO, - Felt::ZERO, - bridge.bridge_admin_id.suffix(), - bridge.bridge_admin_id.prefix().as_felt(), - ]); - let ger_manager_word = Word::new([ - Felt::ZERO, - Felt::ZERO, - bridge.ger_manager_id.suffix(), - bridge.ger_manager_id.prefix().as_felt(), - ]); - - let bridge_storage_slots = vec![ - StorageSlot::with_empty_map(GER_MAP_SLOT_NAME.clone()), - StorageSlot::with_empty_map(LET_FRONTIER_SLOT_NAME.clone()), - StorageSlot::with_value(LET_ROOT_LO_SLOT_NAME.clone(), Word::empty()), - StorageSlot::with_value(LET_ROOT_HI_SLOT_NAME.clone(), Word::empty()), - StorageSlot::with_value(LET_NUM_LEAVES_SLOT_NAME.clone(), Word::empty()), - StorageSlot::with_empty_map(FAUCET_REGISTRY_SLOT_NAME.clone()), - StorageSlot::with_value(BRIDGE_ADMIN_SLOT_NAME.clone(), bridge_admin_word), - StorageSlot::with_value(GER_MANAGER_SLOT_NAME.clone(), ger_manager_word), - ]; - bridge_component(bridge_storage_slots) - } -} - -/// Creates an Agglayer Faucet component with the specified storage slots. -/// -/// This component combines network faucet functionality with bridge validation -/// via Foreign Procedure Invocation (FPI). It provides a "claim" procedure that -/// validates CLAIM notes against a bridge MMR account before minting assets. -fn agglayer_faucet_component(storage_slots: Vec) -> AccountComponent { - let library = agglayer_faucet_component_library(); - let metadata = AccountComponentMetadata::new("agglayer::faucet") - .with_description("AggLayer faucet component with bridge validation") - .with_supported_type(AccountType::FungibleFaucet); - - AccountComponent::new(library, storage_slots, metadata).expect( - "agglayer_faucet component should satisfy the requirements of a valid account component", - ) -} - -// FAUCET CONVERSION STORAGE HELPERS -// ================================================================================================ - -/// Builds the two storage slot values for faucet conversion metadata. -/// -/// The conversion metadata is stored in two value storage slots: -/// - Slot 1 (`miden::agglayer::faucet::conversion_info_1`): `[addr0, addr1, addr2, addr3]` — first -/// 4 felts of the origin token address (5 × u32 limbs). -/// - Slot 2 (`miden::agglayer::faucet::conversion_info_2`): `[addr4, origin_network, scale, 0]` — -/// remaining address felt + origin network + scale factor. -/// -/// # Parameters -/// - `origin_token_address`: The EVM token address in Ethereum format -/// - `origin_network`: The origin network/chain ID -/// - `scale`: The decimal scaling factor (exponent for 10^scale) -/// -/// # Returns -/// A tuple of two `Word` values representing the two storage slot contents. -fn agglayer_faucet_conversion_slots( - origin_token_address: &EthAddressFormat, - origin_network: u32, - scale: u8, -) -> (Word, Word) { - let addr_elements = origin_token_address.to_elements(); - - let slot1 = Word::new([addr_elements[0], addr_elements[1], addr_elements[2], addr_elements[3]]); - - let slot2 = - Word::new([addr_elements[4], Felt::from(origin_network), Felt::from(scale), Felt::ZERO]); - - (slot1, slot2) -} - -// AGGLAYER FAUCET STRUCT -// ================================================================================================ - -static AGGLAYER_FAUCET_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::agglayer::faucet") - .expect("agglayer faucet storage slot name should be valid") -}); -static CONVERSION_INFO_1_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::agglayer::faucet::conversion_info_1") - .expect("conversion info 1 storage slot name should be valid") -}); -static CONVERSION_INFO_2_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::agglayer::faucet::conversion_info_2") - .expect("conversion info 2 storage slot name should be valid") -}); - -/// An [`AccountComponent`] implementing the AggLayer Faucet. -/// -/// It reexports the procedures from `miden::agglayer::faucet`. When linking against this -/// component, the `agglayer` library must be available to the assembler. -/// The procedures of this component are: -/// - `claim`, which validates a CLAIM note against one of the stored GERs in the bridge. -/// - `asset_to_origin_asset`, which converts an asset to the origin asset (used in FPI from -/// bridge). -/// - `burn`, which burns an asset. -/// -/// ## Storage Layout -/// -/// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. -/// - [`Self::bridge_account_id_slot`]: Stores the AggLayer bridge account ID. -/// - [`Self::conversion_info_1_slot`]: Stores the first 4 felts of the origin token address. -/// - [`Self::conversion_info_2_slot`]: Stores the remaining 5th felt of the origin token address + -/// origin network + scale. -#[derive(Debug, Clone)] -pub struct AggLayerFaucet { - metadata: TokenMetadata, - bridge_account_id: AccountId, - origin_token_address: EthAddressFormat, - origin_network: u32, - scale: u8, -} - -impl AggLayerFaucet { - /// Creates a new AggLayer faucet component from the given configuration. - /// - /// # Errors - /// Returns an error if: - /// - The decimals parameter exceeds maximum value of [`TokenMetadata::MAX_DECIMALS`]. - /// - The max supply exceeds maximum possible amount for a fungible asset. - /// - The token supply exceeds the max supply. - pub fn new( - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, - token_supply: Felt, - bridge_account_id: AccountId, - origin_token_address: EthAddressFormat, - origin_network: u32, - scale: u8, - ) -> Result { - let metadata = TokenMetadata::with_supply(symbol, decimals, max_supply, token_supply)?; - Ok(Self { - metadata, - bridge_account_id, - origin_token_address, - origin_network, - scale, - }) - } - - /// Sets the token supply for an existing faucet (e.g. for testing scenarios). - /// - /// # Errors - /// Returns an error if the token supply exceeds the max supply. - pub fn with_token_supply(mut self, token_supply: Felt) -> Result { - self.metadata = self.metadata.with_token_supply(token_supply)?; - Ok(self) - } - - /// Storage slot name for [`TokenMetadata`]. - pub fn metadata_slot() -> &'static StorageSlotName { - TokenMetadata::metadata_slot() - } - - /// Storage slot name for the AggLayer bridge account ID. - pub fn bridge_account_id_slot() -> &'static StorageSlotName { - &AGGLAYER_FAUCET_SLOT_NAME - } - - /// Storage slot name for the first 4 felts of the origin token address. - pub fn conversion_info_1_slot() -> &'static StorageSlotName { - &CONVERSION_INFO_1_SLOT_NAME - } - - /// Storage slot name for the 5th felt of the origin token address, origin network, and scale. - pub fn conversion_info_2_slot() -> &'static StorageSlotName { - &CONVERSION_INFO_2_SLOT_NAME - } -} - -impl From for AccountComponent { - fn from(faucet: AggLayerFaucet) -> Self { - let metadata_slot = StorageSlot::from(faucet.metadata); - - let bridge_account_id_word = Word::new([ - Felt::ZERO, - Felt::ZERO, - faucet.bridge_account_id.suffix(), - faucet.bridge_account_id.prefix().as_felt(), - ]); - let bridge_slot = - StorageSlot::with_value(AGGLAYER_FAUCET_SLOT_NAME.clone(), bridge_account_id_word); - - let (conversion_slot1_word, conversion_slot2_word) = agglayer_faucet_conversion_slots( - &faucet.origin_token_address, - faucet.origin_network, - faucet.scale, - ); - let conversion_slot1 = - StorageSlot::with_value(CONVERSION_INFO_1_SLOT_NAME.clone(), conversion_slot1_word); - let conversion_slot2 = - StorageSlot::with_value(CONVERSION_INFO_2_SLOT_NAME.clone(), conversion_slot2_word); - - let agglayer_storage_slots = - vec![metadata_slot, bridge_slot, conversion_slot1, conversion_slot2]; - agglayer_faucet_component(agglayer_storage_slots) - } -} - -// FAUCET REGISTRY HELPERS -// ================================================================================================ - -/// Creates a faucet registry map key from a faucet account ID. -/// -/// The key format is `[0, 0, faucet_id_suffix, faucet_id_prefix]`. -pub fn faucet_registry_key(faucet_id: AccountId) -> Word { - Word::new([Felt::ZERO, Felt::ZERO, faucet_id.suffix(), faucet_id.prefix().as_felt()]) -} - // AGGLAYER ACCOUNT CREATION HELPERS // ================================================================================================ diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 8b40e17584..5b5f02b920 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -13,13 +13,7 @@ use miden_agglayer::{ use miden_crypto::rand::FeltRng; use miden_protocol::Felt; use miden_protocol::account::auth::AuthScheme; -use miden_protocol::account::{ - Account, - AccountId, - AccountIdVersion, - AccountStorageMode, - AccountType, -}; +use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::note::{NoteAssets, NoteScript, NoteType}; use miden_protocol::transaction::OutputNote; @@ -30,44 +24,6 @@ use miden_tx::utils::hex_to_bytes; use super::test_utils::SOLIDITY_MMR_FRONTIER_VECTORS; -/// Reads the Local Exit Root (double-word) from the bridge account's storage. -/// -/// The Local Exit Root is stored in two dedicated value slots: -/// - [`AggLayerBridge::ler_lo_slot_name`] — low word of the root -/// - [`AggLayerBridge::ler_hi_slot_name`] — high word of the root -/// -/// Returns the 256-bit root as 8 `Felt`s: first the 4 elements of `root_lo` (in -/// reverse of their storage order), followed by the 4 elements of `root_hi` (also in -/// reverse of their storage order). For an empty/uninitialized tree, all elements are -/// zeros. -fn read_local_exit_root(account: &Account) -> Vec { - let root_lo_slot = AggLayerBridge::ler_lo_slot_name(); - let root_hi_slot = AggLayerBridge::ler_hi_slot_name(); - - let root_lo = account - .storage() - .get_item(root_lo_slot) - .expect("should be able to read LET root lo"); - let root_hi = account - .storage() - .get_item(root_hi_slot) - .expect("should be able to read LET root hi"); - - let mut root = Vec::with_capacity(8); - root.extend(root_lo.to_vec().into_iter().rev()); - root.extend(root_hi.to_vec().into_iter().rev()); - root -} - -fn read_let_num_leaves(account: &Account) -> u64 { - let num_leaves_slot = AggLayerBridge::let_num_leaves_slot_name(); - let value = account - .storage() - .get_item(num_leaves_slot) - .expect("should be able to read LET num leaves"); - value.to_vec()[0].as_int() -} - /// Tests that 32 sequential B2AGG note consumptions match all 32 Solidity MMR roots. /// /// This test exercises the complete bridge-out lifecycle: @@ -242,7 +198,7 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { bridge_account.apply_delta(executed_tx.account_delta())?; assert_eq!( - read_let_num_leaves(&bridge_account), + AggLayerBridge::read_let_num_leaves(&bridge_account), (i + 1) as u64, "LET leaf count should match consumed notes" ); @@ -250,7 +206,7 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { let expected_ler = ExitRoot::new(hex_to_bytes(&vectors.roots[i]).expect("valid root hex")).to_elements(); assert_eq!( - read_local_exit_root(&bridge_account), + AggLayerBridge::read_local_exit_root(&bridge_account)?, expected_ler, "Local Exit Root after {} leaves should match the Solidity-generated root", i + 1 diff --git a/crates/miden-testing/tests/agglayer/config_bridge.rs b/crates/miden-testing/tests/agglayer/config_bridge.rs index b9f7dcfbbc..3f2ed47b80 100644 --- a/crates/miden-testing/tests/agglayer/config_bridge.rs +++ b/crates/miden-testing/tests/agglayer/config_bridge.rs @@ -1,11 +1,7 @@ extern crate alloc; -use miden_agglayer::{ - AggLayerBridge, - ConfigAggBridgeNote, - create_existing_bridge_account, - faucet_registry_key, -}; +use miden_agglayer::faucet::faucet_registry_key; +use miden_agglayer::{AggLayerBridge, ConfigAggBridgeNote, create_existing_bridge_account}; use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; use miden_protocol::crypto::rand::FeltRng; diff --git a/crates/miden-testing/tests/agglayer/faucet_helpers.rs b/crates/miden-testing/tests/agglayer/faucet_helpers.rs new file mode 100644 index 0000000000..ecc2f30b5b --- /dev/null +++ b/crates/miden-testing/tests/agglayer/faucet_helpers.rs @@ -0,0 +1,60 @@ +extern crate alloc; + +use miden_agglayer::{ + AggLayerFaucet, + EthAddressFormat, + create_existing_agglayer_faucet, + create_existing_bridge_account, +}; +use miden_protocol::Felt; +use miden_protocol::account::auth::AuthScheme; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::crypto::rand::FeltRng; +use miden_testing::{Auth, MockChain}; + +#[test] +fn test_faucet_helper_methods() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let bridge_admin = + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + let ger_manager = + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + + let bridge_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); + builder.add_account(bridge_account.clone())?; + + let token_symbol = "AGG"; + let decimals = 8u8; + 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_network = 42u32; + let scale = 6u8; + + let faucet = create_existing_agglayer_faucet( + builder.rng_mut().draw_word(), + token_symbol, + decimals, + max_supply, + token_supply, + bridge_account.id(), + &origin_token_address, + origin_network, + scale, + ); + + assert_eq!(AggLayerFaucet::bridge_account_id(&faucet)?, bridge_account.id()); + assert_eq!(AggLayerFaucet::origin_token_address(&faucet)?, origin_token_address); + assert_eq!(AggLayerFaucet::origin_network(&faucet)?, origin_network); + assert_eq!(AggLayerFaucet::scale(&faucet)?, scale); + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index a497f74230..5d84a0cc9d 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -2,6 +2,7 @@ pub mod asset_conversion; mod bridge_in; mod bridge_out; mod config_bridge; +mod faucet_helpers; mod global_index; mod leaf_utils; mod mmr_frontier; diff --git a/crates/miden-testing/tests/agglayer/update_ger.rs b/crates/miden-testing/tests/agglayer/update_ger.rs index 910046db96..1d2f560d51 100644 --- a/crates/miden-testing/tests/agglayer/update_ger.rs +++ b/crates/miden-testing/tests/agglayer/update_ger.rs @@ -16,9 +16,7 @@ use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; use miden_core_lib::handlers::bytes_to_packed_u32_felts; use miden_core_lib::handlers::keccak256::KeccakPreimage; -use miden_crypto::hash::rpo::Rpo256 as Hasher; -use miden_crypto::{Felt, FieldElement}; -use miden_protocol::Word; +use miden_crypto::Felt; use miden_protocol::account::auth::AuthScheme; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::transaction::OutputNote; @@ -99,29 +97,8 @@ async fn update_ger_note_updates_storage() -> anyhow::Result<()> { let mut updated_bridge_account = bridge_account.clone(); updated_bridge_account.apply_delta(executed_transaction.account_delta())?; - // Compute the expected GER hash: rpo256::merge(GER_UPPER, GER_LOWER) - let mut ger_lower: [Felt; 4] = ger.to_elements()[0..4].try_into().unwrap(); - let mut ger_upper: [Felt; 4] = ger.to_elements()[4..8].try_into().unwrap(); - // Elements are reversed: rpo256::merge treats stack as if loaded BE from memory - // The following will produce matching hashes: - // Rust - // Hasher::merge(&[a, b, c, d], &[e, f, g, h]) - // MASM - // rpo256::merge(h, g, f, e, d, c, b, a) - ger_lower.reverse(); - ger_upper.reverse(); - - let ger_hash = Hasher::merge(&[ger_upper.into(), ger_lower.into()]); - // Look up the GER hash in the map storage - let ger_storage_slot = AggLayerBridge::ger_map_slot_name(); - let stored_value = updated_bridge_account - .storage() - .get_map_item(ger_storage_slot, ger_hash) - .expect("GER hash should be stored in the map"); - - // The stored value should be [GER_KNOWN_FLAG, 0, 0, 0] = [1, 0, 0, 0] - let expected_value: Word = [Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO].into(); - assert_eq!(stored_value, expected_value, "GER hash should map to [1, 0, 0, 0]"); + let is_registered = AggLayerBridge::is_ger_registered(ger, updated_bridge_account)?; + assert!(is_registered, "GER was not registered in the bridge account"); Ok(()) }