Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions crates/miden-agglayer/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ The `CLAIM` note is consumed by the bridge account:
The faucet consumes the `MINT` note, mints the specified amount, and creates a [`P2ID`](#46-p2id-generated) note
that delivers the minted assets to the recipient's Miden account.

TODO: Destination network from the leaf data is not validated against Miden's own network
ID ([#2698](https://github.com/0xMiden/protocol/issues/2698)).
Inside `bridge_in::claim`, immediately after proof and leaf data are piped into memory, the bridge asserts the leaf's `destination_network` equals the global MASM constant `MIDEN_NETWORK_ID` in `asm/agglayer/common/constants.masm` (after `swap_u32_bytes` on the LE-packed memory limb). The same value is exposed to Rust as `AggLayerBridge::MIDEN_NETWORK_ID`, matching Solidity test vectors.
This mirrors Solidity `claimAsset` destination-network checks.

TODO: The leaf type field is not validated to be `LEAF_TYPE_ASSET` (0)
([#2699](https://github.com/0xMiden/protocol/issues/2699)).
Expand Down Expand Up @@ -223,12 +223,14 @@ Asserts the note sender matches the GER manager stored in
| **Inputs** | `[PROOF_DATA_KEY, LEAF_DATA_KEY, faucet_mint_amount, pad(7)]` on the operand stack; proof data and leaf data in the advice map keyed by `PROOF_DATA_KEY` and `LEAF_DATA_KEY` respectively |
| **Outputs** | `[pad(16)]` |
| **Context** | Consuming a `CLAIM` note on the bridge account |
| **Panics** | GER not known; global index invalid; Merkle proof verification failed; origin token address not in token registry; claim already spent; amount conversion mismatch |
| **Panics** | Leaf `destination_network` does not match `agglayer::common::constants::MIDEN_NETWORK_ID`; invalid leaf type; GER not known; global index invalid; Merkle proof verification failed; origin token address not in token registry; claim already spent; amount conversion mismatch |

Validates a bridge-in claim and creates a MINT note targeting the faucet:

1. Pipes proof data and leaf data from the advice map into memory, verifying preimage
integrity.
integrity, then asserts the leaf's `destination_network` matches the global
`MIDEN_NETWORK_ID` constant (`asm/agglayer/common/constants.masm`) after `swap_u32_bytes` on
the LE-packed limb (same convention as other AggLayer bridge-in u32 felts in memory).
2. Extracts the destination account ID from the leaf data's destination address
(via `eth_address::to_account_id`).
3. Validates the Merkle proof via `verify_leaf_bridge`: computes the leaf
Expand Down Expand Up @@ -267,7 +269,7 @@ Validates a bridge-in claim and creates a MINT note targeting the faucet:
| `agglayer::bridge::ger_manager_account_id` | 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_account_id` and `ger_manager_account_id` which are set at account creation time.
`admin_account_id` and `ger_manager_account_id` (set at account creation time).

### 3.2 Faucet Account Component

Expand Down Expand Up @@ -495,9 +497,9 @@ The storage is divided into three logical regions: proof data (felts 0-535), lea
advice map as two keyed entries (`PROOF_DATA_KEY`, `LEAF_DATA_KEY`).
4. The `miden_claim_amount` is read from memory.
5. `bridge_in::claim` is called with `[PROOF_DATA_KEY, LEAF_DATA_KEY, miden_claim_amount]`
on the stack. The bridge validates the proof, checks the claim nullifier, looks up the
faucet via the token registry, verifies the amount conversion, then builds a MINT
output note targeting the faucet.
on the stack. The bridge asserts the leaf's `destination_network` matches the global
`MIDEN_NETWORK_ID` MASM constant, validates the proof, checks the claim nullifier, looks up the faucet via the token
registry, verifies the amount conversion, then builds a MINT output note targeting the faucet.

#### Permissions

Expand Down
82 changes: 61 additions & 21 deletions crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use agglayer::bridge::bridge_config
use agglayer::bridge::leaf_utils
use agglayer::common::constants::MIDEN_NETWORK_ID
use agglayer::common::utils
use agglayer::common::asset_conversion
use agglayer::common::eth_address
Expand Down Expand Up @@ -34,6 +35,7 @@ const ERR_SMT_ROOT_VERIFICATION_FAILED = "merkle proof verification failed: prov
const ERR_CLAIM_ALREADY_SPENT = "claim note has already been spent"
const ERR_SOURCE_BRIDGE_NETWORK_OVERFLOW = "source bridge network overflowed u32"
const ERR_INVALID_LEAF_TYPE = "invalid leaf type: only asset claims (leafType=0) are supported"
const ERR_CLAIM_LEAF_DESTINATION_NETWORK_MISMATCH = "claim leaf destination network does not match Miden AggLayer network ID"

# CONSTANTS
# =================================================================================================
Expand Down Expand Up @@ -112,26 +114,31 @@ const CLAIM_LEAF_DATA_KEY_MEM_ADDR = 704
const CLAIM_LEAF_INDEX_MEM_ADDR = 900
const CLAIM_SOURCE_BRIDGE_NETWORK_MEM_ADDR = 901

# Memory addresses for leaf data fields (derived from leaf data layout at CLAIM_LEAF_DATA_START_PTR=536)
const LEAF_TYPE_ADDRESS = 536
const ORIGIN_TOKEN_ADDRESS_0 = 538
const ORIGIN_TOKEN_ADDRESS_1 = 539
const ORIGIN_TOKEN_ADDRESS_2 = 540
const ORIGIN_TOKEN_ADDRESS_3 = 541
const ORIGIN_TOKEN_ADDRESS_4 = 542
const DESTINATION_ADDRESS_0 = 544
const DESTINATION_ADDRESS_1 = 545
const DESTINATION_ADDRESS_2 = 546
const DESTINATION_ADDRESS_3 = 547
const DESTINATION_ADDRESS_4 = 548
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = 549
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = 550
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_2 = 551
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_3 = 552
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_4 = 553
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 = 554
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 = 555
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 = 556
# Memory addresses for leaf data fields (felts relative to CLAIM_LEAF_DATA_START_PTR):
const LEAF_TYPE_ADDRESS = CLAIM_LEAF_DATA_START_PTR

const ORIGIN_TOKEN_ADDRESS_0 = CLAIM_LEAF_DATA_START_PTR + 2
const ORIGIN_TOKEN_ADDRESS_1 = CLAIM_LEAF_DATA_START_PTR + 3
const ORIGIN_TOKEN_ADDRESS_2 = CLAIM_LEAF_DATA_START_PTR + 4
const ORIGIN_TOKEN_ADDRESS_3 = CLAIM_LEAF_DATA_START_PTR + 5
const ORIGIN_TOKEN_ADDRESS_4 = CLAIM_LEAF_DATA_START_PTR + 6
Comment on lines +118 to +124
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a nice addition but the changes are unrelated to the feature described in this PR; ideally should have been a separate PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but I was updating this addresses anyway, and since this change is quite small, I decide to include it into this PR.


const DESTINATION_NETWORK_ID_MEM_ADDR = CLAIM_LEAF_DATA_START_PTR + 7

const DESTINATION_ADDRESS_0 = CLAIM_LEAF_DATA_START_PTR + 8
const DESTINATION_ADDRESS_1 = CLAIM_LEAF_DATA_START_PTR + 9
const DESTINATION_ADDRESS_2 = CLAIM_LEAF_DATA_START_PTR + 10
const DESTINATION_ADDRESS_3 = CLAIM_LEAF_DATA_START_PTR + 11
const DESTINATION_ADDRESS_4 = CLAIM_LEAF_DATA_START_PTR + 12

const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = CLAIM_LEAF_DATA_START_PTR + 13
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = CLAIM_LEAF_DATA_START_PTR + 14
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_2 = CLAIM_LEAF_DATA_START_PTR + 15
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_3 = CLAIM_LEAF_DATA_START_PTR + 16
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_4 = CLAIM_LEAF_DATA_START_PTR + 17
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 = CLAIM_LEAF_DATA_START_PTR + 18
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 = CLAIM_LEAF_DATA_START_PTR + 19
const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 = CLAIM_LEAF_DATA_START_PTR + 20

# Memory addresses for MINT note output construction
const MINT_NOTE_STORAGE_MEM_ADDR_0 = 800
Expand Down Expand Up @@ -193,6 +200,7 @@ const CLAIM_DEST_ID_SUFFIX_LOCAL = 1
#!
#! Panics if:
#! - the leaf type is not 0 (not an asset claim).
#! - the leaf destination network does not match the global `MIDEN_NETWORK_ID` constant.
#! - the Merkle proof validation fails.
#! - the origin token address is not registered in the bridge's token registry.
#!
Expand All @@ -207,6 +215,10 @@ pub proc claim
exec.claim_batch_pipe_double_words
# => [pad(16)]

# check that the destination network stored in the leaf data matches the Miden network ID
exec.assert_claim_leaf_destination_network
# => [pad(16)]

exec.load_destination_address
exec.eth_address::to_account_id
loc_store.CLAIM_DEST_ID_SUFFIX_LOCAL loc_store.CLAIM_DEST_ID_PREFIX_LOCAL
Expand Down Expand Up @@ -529,7 +541,7 @@ pub proc get_leaf_value(leaf_data_key: word) -> DoubleWord
exec.mem::pipe_preimage_to_memory drop
# => []

# compute the leaf value for elements in memory starting at LEAF_DATA_START_PTR
# compute the leaf value from elements in memory starting at LEAF_DATA_START_PTR
push.LEAF_DATA_START_PTR
exec.leaf_utils::compute_leaf_value
# => [LEAF_VALUE[8]]
Expand Down Expand Up @@ -1131,3 +1143,31 @@ proc store_cgi_chain_hash
exec.native_account::set_item dropw
# => []
end

#! Asserts the claim leaf's `destination_network` matches the global `MIDEN_NETWORK_ID`.
#!
#! `claim_batch_pipe_double_words` stores leaf felts as LE-packed u32 limbs. `swap_u32_bytes`
#! converts the loaded limb to the canonical u32 value so it can be compared to `MIDEN_NETWORK_ID`
#! from `agglayer::common::constants`.
#!
#! Inputs: []
#! Outputs: []
#!
#! Panics if:
#! - the leaf destination network does not match the Miden AggLayer network ID constant.
#!
#! Invocation: exec
proc assert_claim_leaf_destination_network
# load the destination network ID onto the stack
mem_load.DESTINATION_NETWORK_ID_MEM_ADDR
# => [destination_network_id_le]

# change the endianness to BE to compare it with the Miden network ID
exec.utils::swap_u32_bytes
# => [destination_network_id_be]

# assert that the destination network ID matches the Miden network ID
push.MIDEN_NETWORK_ID
assert_eq.err=ERR_CLAIM_LEAF_DESTINATION_NETWORK_MISMATCH
# => []
end
10 changes: 10 additions & 0 deletions crates/miden-agglayer/asm/agglayer/common/constants.masm
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# AggLayer-wide numeric constants shared by bridge-in and other modules.
#
# Each `const NAME = <decimal u32 value>` line is parsed at crate build time into
# `pub const NAME: u32 = value` in `agglayer_constants.rs` (see `build.rs`).

# NETWORK IDS
# =================================================================================================

# AggLayer-assigned network ID for this Miden chain.
const MIDEN_NETWORK_ID = 77
80 changes: 78 additions & 2 deletions crates/miden-agglayer/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashSet;
use std::env;
use std::fmt::Write;
use std::path::Path;
Expand All @@ -17,6 +18,7 @@ use miden_protocol::account::{
use miden_protocol::transaction::TransactionKernel;
use miden_standards::account::auth::NoAuth;
use miden_standards::account::mint_policies::OwnerControlled;
use regex::Regex;

// CONSTANTS
// ================================================================================================
Expand All @@ -26,6 +28,7 @@ const ASM_DIR: &str = "asm";
const ASM_NOTE_SCRIPTS_DIR: &str = "note_scripts";
const ASM_AGGLAYER_DIR: &str = "agglayer";
const ASM_AGGLAYER_BRIDGE_DIR: &str = "agglayer/bridge";
const ASM_AGGLAYER_CONSTANTS_MASM: &str = "agglayer/common/constants.masm";
const ASM_COMPONENTS_DIR: &str = "components";

const AGGLAYER_ERRORS_RS_FILE: &str = "agglayer_errors.rs";
Expand Down Expand Up @@ -80,7 +83,12 @@ fn main() -> Result<()> {

// 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)?;
let agglayer_constants_masm_path = crate_path.join(ASM_DIR).join(ASM_AGGLAYER_CONSTANTS_MASM);
generate_agglayer_constants(
constants_out_path,
component_libraries,
&agglayer_constants_masm_path,
)?;

generate_error_constants(&source_dir, &build_dir)?;

Expand Down Expand Up @@ -207,14 +215,77 @@ fn compile_account_components(
// GENERATE AGGLAYER CONSTANTS
// ================================================================================================

/// Parses every decimal `u32` constant from `asm/agglayer/common/constants.masm`.
///
/// Recognized lines (whitespace-flexible, one definition per line, `#` comments ignored by the
/// regex):
///
/// ```text
/// const SOME_NAME = 123
/// ```
///
/// Each match is emitted to `agglayer_constants.rs` as `pub const SOME_NAME: u32`.
/// Duplicate `const` names in the same file are a build error. Non-decimal values (e.g. `word(...)`
/// or array literals) are not parsed here; add support in this function when needed.
fn parse_numeric_constants_from_constants_masm(masm_path: &Path) -> Result<Vec<(String, u32)>> {
// Read the full `constants.masm` text; parsing is line-based so we need the whole file.
let contents = fs::read_to_string(masm_path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read {}", masm_path.display()))?;

// One line per match: optional leading space, `const`, identifier (no leading digit), `=`,
// decimal digits only. `(?m)^` makes `^` match after newlines so we skip comment-only lines.
let re = Regex::new(r"(?m)^\s*const\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(\d+)\s*$")
.expect("constants.masm parse regex should compile");

// `out` preserves declaration order; `seen` rejects duplicate const names in the same file.
let mut out = Vec::new();
let mut seen = HashSet::new();

for caps in re.captures_iter(&contents) {
let name = caps.get(1).expect("group 1").as_str();

// Require each identifier at most once so generated Rust names are unique.
if !seen.insert(name.to_string()) {
return Err(Report::msg(format!(
"duplicate `const {name}` in {}",
masm_path.display()
)));
}

// Right-hand side must fit `u32` (same range we emit in Rust).
let raw = caps.get(2).expect("group 2").as_str();
let value = raw.parse::<u32>().map_err(|_| {
Report::msg(format!(
"`const {name}` value `{raw}` is not a valid u32 in {}",
masm_path.display()
))
})?;

out.push((name.to_string(), value));
}

// Empty match set is almost certainly a misconfigured or mistyped `constants.masm`.
if out.is_empty() {
return Err(Report::msg(format!(
"{} does not contain any constants to parse",
masm_path.display()
)));
}

Ok(out)
}

/// Generates a Rust file containing AggLayer specific constants.
///
/// At the moment, this file contains the following constants:
/// This file contains:
/// - All the constants listed in the `constants.masm` file.
/// - AggLayer Bridge code commitment.
/// - AggLayer Faucet code commitment.
fn generate_agglayer_constants(
target_file: impl AsRef<Path>,
component_libraries: Vec<(String, Library)>,
constants_masm_path: &Path,
) -> Result<()> {
let mut file_contents = String::new();

Expand All @@ -232,6 +303,11 @@ fn generate_agglayer_constants(
)
.unwrap();

let masm_constants = parse_numeric_constants_from_constants_masm(constants_masm_path)?;
for (name, value) in &masm_constants {
writeln!(file_contents, "pub const {name}: u32 = {value};\n").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", AccountType::all());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"amount": "100000000000000000000",
"claimed_global_index_hash_chain": "0xbce0afc98c69ea85e9cfbf98c87c58a77c12d857551f1858530341392f70c22d",
"claimed_global_index_hash_chain": "0xaaa935b34029c1f61586245a57893b461fd7b25fde618c4fd64accb92a8b42f1",
"deposit_count": 1,
"description": "L1 bridgeAsset transaction test vectors with valid Merkle proofs",
"destination_address": "0x00000000AA0000000000bb000000cc000000Dd00",
"destination_network": 20,
"global_exit_root": "0xc84f1e3744c151b345a8899034b3677c0fdbaf45aa3aaf18a3f97dbcf70836cb",
"destination_network": 77,
"global_exit_root": "0x1e79b6d6b557b9404242f0118ca5bbef4b2017209abf978628b456e684985b10",
"global_index": "0x0000000000000000000000000000000000000000000000010000000000000000",
"leaf_type": 0,
"leaf_value": "0x9d85d7c56264697df18f458b4b12a457b87b7e7f7a9b16dcb368514729ef680d",
"local_exit_root": "0xc9e095ea4cfe19b7e9a6d1aff6c55914ccc8df34954f9f6a2ad8e42d2632a0ab",
"mainnet_exit_root": "0xc9e095ea4cfe19b7e9a6d1aff6c55914ccc8df34954f9f6a2ad8e42d2632a0ab",
"leaf_value": "0x0d16f063db3b2ea0c1ff3f41538b644820f4eb718aafc57006823bf0ccbabc81",
"local_exit_root": "0xe6fc31195b37bad51b5e013b283bb9e6eba87719f2d8df0c9524d25a1765ce44",
"mainnet_exit_root": "0xe6fc31195b37bad51b5e013b283bb9e6eba87719f2d8df0c9524d25a1765ce44",
"metadata": "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a5465737420546f6b656e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000045445535400000000000000000000000000000000000000000000000000000000",
"metadata_hash": "0x4d0d9fb7f9ab2f012da088dc1c228173723db7e09147fe4fea2657849d580161",
"origin_network": 0,
Expand Down
Loading
Loading