Skip to content

Commit 5572686

Browse files
mmagicianclaude
andauthored
feat(agglayer): make bridge network ID a deployment setting (#3062)
* feat(agglayer): make bridge network ID a deployment setting The bridge's AggLayer network ID was hardcoded as the MASM constant MIDEN_NETWORK_ID and compiled directly into the bridge contract, baking it into the bridge code commitment. This forced testnet/mainnet, which may use different network IDs, to ship different bridge binaries. Store the network ID in the bridge account instead: - Add a `network_id` value storage slot (`agglayer::bridge::network_id`), written once at account creation. - `bridge_in`/`bridge_out` read it via a new `bridge_config::load_network_id` helper instead of `push.MIDEN_NETWORK_ID`, so the bridge code commitment is identical across all networks. - Require `network_id` in `AggLayerBridge::new` and `create_(existing_)bridge_account`; add an `AggLayerBridge::network_id` reader and `network_id_slot_name` accessor. - Remove the hardcoded `MIDEN_NETWORK_ID` constant entirely (MASM `constants.masm` and its build.rs parsing). Tests keep a fixture-only constant matching the bundled Solidity claim vectors. The ID is never mutated by any bridge procedure, so it is effectively immutable for the life of the account. Adds bridge-in and bridge-out tests proving a bridge configured with a non-default network ID enforces it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Apply suggestions from code review Co-authored-by: Marti <marcin.gorny.94@protonmail.com> * fix: remove unused AggLayerBridge import in bridge_in test The top-level import is shadowed by a function-local `use` in test_bridge_in_claim_to_p2id, so clippy flags it as unused with -D warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude (Opus) <noreply@anthropic.com>
1 parent 58cfe10 commit 5572686

17 files changed

Lines changed: 217 additions & 152 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## v0.16.0 (TBD)
4+
5+
- [BREAKING] Changed AggLayerBridge to store its AggLayer network ID in account storage ([#3062](https://github.com/0xMiden/protocol/pull/3062)).
6+
37
## v0.15.2 (2026-06-05)
48

59
- [BREAKING] `AuthNetworkAccount` now gates transaction scripts with a root allowlist instead of banning them outright, enabling network accounts to run approved tx scripts such as setting the expiration delta ([#3028](https://github.com/0xMiden/protocol/pull/3028)).

bin/bench-transaction/src/context_setups.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ use miden_standards::note::StandardNote;
2626
use miden_testing::{Auth, MockChain, TransactionContext};
2727
use rand::Rng;
2828

29+
/// AggLayer network ID encoded in the bundled claim test vectors. Bridges in these benchmark
30+
/// setups are created with this value so vector-based claims validate against the configured ID.
31+
const MIDEN_NETWORK_ID: u32 = 77;
32+
2933
// P2ID NOTE SETUPS
3034
// ================================================================================================
3135

@@ -189,8 +193,12 @@ pub async fn tx_consume_claim_note(data_source: ClaimDataSource) -> Result<Trans
189193

190194
// CREATE BRIDGE ACCOUNT
191195
let bridge_seed = builder.rng_mut().draw_word();
192-
let bridge_account =
193-
create_existing_bridge_account(bridge_seed, bridge_admin.id(), ger_manager.id());
196+
let bridge_account = create_existing_bridge_account(
197+
bridge_seed,
198+
bridge_admin.id(),
199+
ger_manager.id(),
200+
MIDEN_NETWORK_ID,
201+
);
194202
builder.add_account(bridge_account.clone())?;
195203

196204
// GET CLAIM DATA FROM JSON
@@ -402,6 +410,7 @@ pub async fn tx_consume_b2agg_note(pre_populate_leaves: Option<u32>) -> Result<T
402410
builder.rng_mut().draw_word(),
403411
bridge_admin.id(),
404412
ger_manager.id(),
413+
MIDEN_NETWORK_ID,
405414
);
406415

407416
// Pre-populate frontier before adding the account to the mock chain

crates/miden-agglayer/SPEC.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ The `CLAIM` note is consumed by the bridge account:
9393
`P2ID` note targeted at the recipient directly. The asset must have been previously
9494
locked into the bridge by a prior bridge-out for the same token.
9595

96-
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.
96+
Inside `bridge_in::claim`, immediately after proof and leaf data are piped into memory, the bridge asserts the leaf's `destination_network` equals the bridge's configured network ID (after `swap_u32_bytes` on the LE-packed memory limb). The network ID is a deployment setting passed to `create_bridge_account` and stored in the `agglayer::bridge::network_id` slot; `bridge_config::load_network_id` reads it at runtime. It is set once at account creation and never mutated.
9797
This mirrors Solidity `claimAsset` destination-network checks.
9898

9999
TODO: The leaf type field is not validated to be `LEAF_TYPE_ASSET` (0)
@@ -259,14 +259,14 @@ in the map the procedure panics with `ERR_GER_ALREADY_REGISTERED`.
259259
| **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 |
260260
| **Outputs** | `[pad(16)]` |
261261
| **Context** | Consuming a `CLAIM` note on the bridge account |
262-
| **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, origin network) pair not in token registry; claim already spent; amount conversion mismatch |
262+
| **Panics** | Leaf `destination_network` does not match the bridge's configured network ID; invalid leaf type; GER not known; global index invalid; Merkle proof verification failed; (origin token address, origin network) pair not in token registry; claim already spent; amount conversion mismatch |
263263

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

266266
1. Pipes proof data and leaf data from the advice map into memory, verifying preimage
267-
integrity, then asserts the leaf's `destination_network` matches the global
268-
`MIDEN_NETWORK_ID` constant (`asm/agglayer/common/constants.masm`) after `swap_u32_bytes` on
269-
the LE-packed limb (same convention as other AggLayer bridge-in u32 felts in memory).
267+
integrity, then asserts the leaf's `destination_network` matches the bridge's configured
268+
network ID (read from the `agglayer::bridge::network_id` storage slot) after `swap_u32_bytes`
269+
on the LE-packed limb (same convention as other AggLayer bridge-in u32 felts in memory).
270270
2. Extracts the destination account ID from the leaf data's destination address
271271
(via `eth_address::to_account_id`).
272272
3. Validates the Merkle proof via `verify_leaf_bridge`: computes the leaf
@@ -540,8 +540,8 @@ The storage is divided into three logical regions: proof data (felts 0-535), lea
540540
advice map as two keyed entries (`PROOF_DATA_KEY`, `LEAF_DATA_KEY`).
541541
4. The `miden_claim_amount` is read from memory.
542542
5. `bridge_in::claim` is called with `[PROOF_DATA_KEY, LEAF_DATA_KEY, miden_claim_amount]`
543-
on the stack. The bridge asserts the leaf's `destination_network` matches the global
544-
`MIDEN_NETWORK_ID` MASM constant, validates the proof, checks the claim nullifier, looks up the faucet via the token
543+
on the stack. The bridge asserts the leaf's `destination_network` matches the bridge's
544+
configured network ID, validates the proof, checks the claim nullifier, looks up the faucet via the token
545545
registry, verifies the amount conversion, then builds a MINT output note targeting the faucet.
546546

547547
#### Permissions

crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const GER_MAP_STORAGE_SLOT = word("agglayer::bridge::ger_map")
2626
const FAUCET_REGISTRY_MAP_SLOT = word("agglayer::bridge::faucet_registry_map")
2727
const TOKEN_REGISTRY_MAP_SLOT = word("agglayer::bridge::token_registry_map")
2828
const FAUCET_METADATA_MAP_SLOT = word("agglayer::bridge::faucet_metadata_map")
29+
const NETWORK_ID_SLOT = word("agglayer::bridge::network_id")
2930

3031
# Flags
3132
const GER_KNOWN_FLAG = [1, 0, 0, 0]
@@ -541,3 +542,27 @@ proc assert_sender_is_ger_manager
541542
assert.err=ERR_SENDER_NOT_GER_MANAGER
542543
# => [pad(16)]
543544
end
545+
546+
#! Returns the AggLayer network ID assigned to this bridge.
547+
#!
548+
#! The network ID is written to NETWORK_ID_SLOT once at account creation and is never mutated by
549+
#! any bridge procedure, so it is effectively immutable for the lifetime of the account. It
550+
#! identifies this Miden chain within the AggLayer and may differ between deployments (e.g. testnet
551+
#! vs mainnet).
552+
#!
553+
#! Inputs: []
554+
#! Outputs: [network_id]
555+
#!
556+
#! Where:
557+
#! - network_id is the u32 AggLayer network ID of this bridge.
558+
#!
559+
#! Invocation: exec
560+
pub proc load_network_id
561+
push.NETWORK_ID_SLOT[0..2]
562+
exec.active_account::get_item
563+
# => [network_id, 0, 0, 0]
564+
565+
# discard the three padding zeros, keeping only the network ID
566+
movdn.3 drop drop drop
567+
# => [network_id]
568+
end

crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use agglayer::bridge::bridge_config
22
use agglayer::bridge::bridge_in_output
33
use agglayer::bridge::leaf_utils
4-
use agglayer::common::constants::MIDEN_NETWORK_ID
54
use agglayer::common::utils
65
use agglayer::common::asset_conversion
76
use agglayer::common::eth_address
@@ -166,7 +165,7 @@ const CLAIM_DEST_ID_SUFFIX_LOCAL = 1
166165
#!
167166
#! Panics if:
168167
#! - the leaf type is not 0 (not an asset claim).
169-
#! - the leaf destination network does not match the global `MIDEN_NETWORK_ID` constant.
168+
#! - the leaf destination network does not match the bridge's configured network ID.
170169
#! - the Merkle proof validation fails.
171170
#! - the (origin_token_address, origin_network) pair is not registered in the bridge's token registry.
172171
#!
@@ -932,30 +931,32 @@ proc store_cgi_chain_hash
932931
# => []
933932
end
934933

935-
#! Asserts the claim leaf's `destination_network` matches the global `MIDEN_NETWORK_ID`.
934+
#! Asserts the claim leaf's `destination_network` matches the bridge's configured network ID.
936935
#!
937936
#! `claim_batch_pipe_double_words` stores leaf felts as LE-packed u32 limbs. `swap_u32_bytes`
938-
#! converts the loaded limb to the canonical u32 value so it can be compared to `MIDEN_NETWORK_ID`
939-
#! from `agglayer::common::constants`.
937+
#! converts the loaded limb to the canonical u32 value so it can be compared to the network ID
938+
#! read from the bridge's storage via `bridge_config::load_network_id`.
940939
#!
941940
#! Inputs: []
942941
#! Outputs: []
943942
#!
944943
#! Panics if:
945-
#! - the leaf destination network does not match the Miden AggLayer network ID constant.
944+
#! - the leaf destination network does not match the bridge's configured network ID.
946945
#!
947946
#! Invocation: exec
948947
proc assert_claim_leaf_destination_network
949948
# load the destination network ID onto the stack
950949
mem_load.DESTINATION_NETWORK_ID_MEM_ADDR
951950
# => [destination_network_id_le]
952951

953-
# change the endianness to BE to compare it with the Miden network ID
952+
# change the endianness to BE to compare it with the network ID
954953
exec.utils::swap_u32_bytes
955954
# => [destination_network_id_be]
956955

957-
# assert that the destination network ID matches the Miden network ID
958-
push.MIDEN_NETWORK_ID
956+
# assert that the destination network ID matches the bridge's configured network ID
957+
exec.bridge_config::load_network_id
958+
# => [network_id, destination_network_id_be]
959+
959960
assert_eq.err=ERR_CLAIM_LEAF_DESTINATION_NETWORK_MISMATCH
960961
# => []
961962
end

crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use miden::standards::note::execution_hint::ALWAYS
1010
use miden::protocol::types::MemoryAddress
1111
use miden::protocol::output_note
1212
use miden::core::crypto::hashes::poseidon2
13-
use agglayer::common::constants::MIDEN_NETWORK_ID
1413
use agglayer::common::utils
1514
use agglayer::common::asset_conversion
1615
use agglayer::bridge::bridge_config
@@ -246,23 +245,29 @@ end
246245
# HELPER PROCEDURES
247246
# =================================================================================================
248247

249-
#! Asserts that the bridge-out destination network ID is not equal to the Miden's AggLayer network
250-
#! ID.
248+
#! Asserts that the bridge-out destination network ID is not equal to the bridge's configured
249+
#! network ID.
251250
#!
252251
#! Inputs: [dest_network_id]
253252
#! Outputs: []
254253
#!
254+
#! Where:
255+
#! - dest_network_id is the LE-packed u32 destination network ID of the B2AGG note.
256+
#!
255257
#! Panics if:
256-
#! - the destination network ID equals `MIDEN_NETWORK_ID`.
258+
#! - the destination network ID equals the bridge's configured network ID.
257259
#!
258260
#! Invocation: exec
259261
proc assert_destination_id_not_miden_id
260-
# change the endianness to BE to compare it with the Miden network ID
262+
# change the endianness to BE to compare it with the network ID
261263
exec.utils::swap_u32_bytes
262264
# => [destination_network_id_be]
263265

264-
# assert that the destination network ID is not equal to the Miden network ID
265-
push.MIDEN_NETWORK_ID neq assert.err=ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN
266+
# assert that the destination network ID is not equal to the bridge's configured network ID
267+
exec.bridge_config::load_network_id
268+
# => [network_id, destination_network_id_be]
269+
270+
neq assert.err=ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN
266271
# => []
267272
end
268273

crates/miden-agglayer/asm/agglayer/common/constants.masm

Lines changed: 0 additions & 10 deletions
This file was deleted.

crates/miden-agglayer/build.rs

Lines changed: 2 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::{BTreeSet, HashSet};
1+
use std::collections::BTreeSet;
22
use std::env;
33
use std::fmt::Write;
44
use std::path::Path;
@@ -21,7 +21,6 @@ use miden_standards::account::policies::{
2121
TokenPolicyManager,
2222
TransferPolicy,
2323
};
24-
use regex::Regex;
2524

2625
// CONSTANTS
2726
// ================================================================================================
@@ -31,7 +30,6 @@ const ASM_DIR: &str = "asm";
3130
const ASM_NOTE_SCRIPTS_DIR: &str = "note_scripts";
3231
const ASM_AGGLAYER_DIR: &str = "agglayer";
3332
const ASM_AGGLAYER_BRIDGE_DIR: &str = "agglayer/bridge";
34-
const ASM_AGGLAYER_CONSTANTS_MASM: &str = "agglayer/common/constants.masm";
3533
const ASM_COMPONENTS_DIR: &str = "components";
3634

3735
const AGGLAYER_ERRORS_RS_FILE: &str = "agglayer_errors.rs";
@@ -86,12 +84,7 @@ fn main() -> Result<()> {
8684

8785
// generate agglayer specific constants
8886
let constants_out_path = Path::new(&build_dir).join(AGGLAYER_GLOBAL_CONSTANTS_FILE_NAME);
89-
let agglayer_constants_masm_path = crate_path.join(ASM_DIR).join(ASM_AGGLAYER_CONSTANTS_MASM);
90-
generate_agglayer_constants(
91-
constants_out_path,
92-
component_libraries,
93-
&agglayer_constants_masm_path,
94-
)?;
87+
generate_agglayer_constants(constants_out_path, component_libraries)?;
9588

9689
generate_error_constants(&source_dir, &build_dir)?;
9790

@@ -217,78 +210,14 @@ fn compile_account_components(
217210
// GENERATE AGGLAYER CONSTANTS
218211
// ================================================================================================
219212

220-
/// Parses every decimal `u32` constant from `asm/agglayer/common/constants.masm`.
221-
///
222-
/// Recognized lines (whitespace-flexible, one definition per line, `#` comments ignored by the
223-
/// regex):
224-
///
225-
/// ```text
226-
/// const SOME_NAME = 123
227-
/// ```
228-
///
229-
/// Each match is emitted to `agglayer_constants.rs` as `pub const SOME_NAME: u32`.
230-
/// Duplicate `const` names in the same file are a build error. Non-decimal values (e.g. `word(...)`
231-
/// or array literals) are not parsed here; add support in this function when needed.
232-
fn parse_numeric_constants_from_constants_masm(masm_path: &Path) -> Result<Vec<(String, u32)>> {
233-
// Read the full `constants.masm` text; parsing is line-based so we need the whole file.
234-
let contents = fs::read_to_string(masm_path)
235-
.into_diagnostic()
236-
.wrap_err_with(|| format!("failed to read {}", masm_path.display()))?;
237-
238-
// One line per match: optional leading space, optional `pub` visibility, `const`, identifier
239-
// (no leading digit), `=`, decimal digits only. `(?m)^` makes `^` match after newlines so we
240-
// skip comment-only lines.
241-
let re = Regex::new(r"(?m)^\s*(?:pub\s+)?const\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(\d+)\s*$")
242-
.expect("constants.masm parse regex should compile");
243-
244-
// `out` preserves declaration order; `seen` rejects duplicate const names in the same file.
245-
let mut out = Vec::new();
246-
let mut seen = HashSet::new();
247-
248-
for caps in re.captures_iter(&contents) {
249-
let name = caps.get(1).expect("group 1").as_str();
250-
251-
// Require each identifier at most once so generated Rust names are unique.
252-
if !seen.insert(name.to_string()) {
253-
return Err(Report::msg(format!(
254-
"duplicate `const {name}` in {}",
255-
masm_path.display()
256-
)));
257-
}
258-
259-
// Right-hand side must fit `u32` (same range we emit in Rust).
260-
let raw = caps.get(2).expect("group 2").as_str();
261-
let value = raw.parse::<u32>().map_err(|_| {
262-
Report::msg(format!(
263-
"`const {name}` value `{raw}` is not a valid u32 in {}",
264-
masm_path.display()
265-
))
266-
})?;
267-
268-
out.push((name.to_string(), value));
269-
}
270-
271-
// Empty match set is almost certainly a misconfigured or mistyped `constants.masm`.
272-
if out.is_empty() {
273-
return Err(Report::msg(format!(
274-
"{} does not contain any constants to parse",
275-
masm_path.display()
276-
)));
277-
}
278-
279-
Ok(out)
280-
}
281-
282213
/// Generates a Rust file containing AggLayer specific constants.
283214
///
284215
/// This file contains:
285-
/// - All the constants listed in the `constants.masm` file.
286216
/// - AggLayer Bridge code commitment.
287217
/// - AggLayer Faucet code commitment.
288218
fn generate_agglayer_constants(
289219
target_file: impl AsRef<Path>,
290220
component_libraries: Vec<(String, Library)>,
291-
constants_masm_path: &Path,
292221
) -> Result<()> {
293222
let mut file_contents = String::new();
294223

@@ -306,11 +235,6 @@ fn generate_agglayer_constants(
306235
)
307236
.unwrap();
308237

309-
let masm_constants = parse_numeric_constants_from_constants_masm(constants_masm_path)?;
310-
for (name, value) in &masm_constants {
311-
writeln!(file_contents, "pub const {name}: u32 = {value};\n").unwrap();
312-
}
313-
314238
// Create a dummy metadata to be able to create components. We only interested in the resulting
315239
// code commitment, so it doesn't matter what does this metadata holds.
316240
let dummy_metadata = AccountComponentMetadata::new("dummy");

0 commit comments

Comments
 (0)