Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1b350af
feat: implement verify_leaf_bridge
mmagician Jan 16, 2026
5afc32e
chore: stack is empty, can swap instead of pad
mmagician Jan 16, 2026
132356b
chore: load leaf first, proof later
mmagician Jan 16, 2026
e07a9d6
Merge branch 'next' into mmagician-verify-leaf-bridge
mmagician Jan 16, 2026
b31afb1
feat(AggLayer): implement `verify_leaf` (#2295)
mmagician Jan 23, 2026
7dbbf8c
docs: add comment about exit root pointer locations in compute_ger
cursoragent Jan 28, 2026
9070e78
refactor: remove unused verify_claim_proof procedure
cursoragent Jan 28, 2026
86d7153
style: add section headers for better organization in bridge_in.masm
cursoragent Jan 28, 2026
6a62186
Merge branch 'agglayer-fixed-2' into mmagician-verify-leaf-bridge
mmagician Jan 28, 2026
1b3acaf
fix: get leaf value is exec, should not truncate stack
mmagician Jan 28, 2026
e3c2f79
Apply suggestions from code review
mmagician Jan 28, 2026
47615ad
chore: add padding to stack comments
mmagician Feb 1, 2026
39b8863
chore: make padding explicit for update ger
mmagician Feb 1, 2026
4c351da
chore: update_ger proc is call not exec
mmagician Feb 1, 2026
da96ff9
chore: assert_valid_ger is not public
mmagician Feb 1, 2026
e1c0e22
docs: make panics explicit; add verify steps
mmagician Feb 1, 2026
bf68b20
chore: fix indent for inline comment
mmagician Feb 1, 2026
d7b04da
chore: re-org procedures: pub/helper
mmagician Feb 1, 2026
d312f36
chore: Word prefer empty over default
mmagician Feb 1, 2026
301f2bf
changelog: move entry 0.13 -> 0.14
mmagician Feb 1, 2026
ce41b83
Merge branch 'agglayer-fixed-2' into mmagician-verify-leaf-bridge
mmagician Feb 1, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Enable `CodeBuilder` to add advice map entries to compiled scripts ([#2275](https://github.com/0xMiden/miden-base/pull/2275)).
- Added `BlockNumber::MAX` constant to represent the maximum block number ([#2324](https://github.com/0xMiden/miden-base/pull/2324)).
- Added single-word `Array` standard ([#2203](https://github.com/0xMiden/miden-base/pull/2203)).
- Implemented verification of AggLayer deposits (claims) against GER ([#2295](https://github.com/0xMiden/miden-base/pull/2295), [#2288](https://github.com/0xMiden/miden-base/pull/2288)).
- Added `SignedBlock` struct ([#2355](https://github.com/0xMiden/miden-base/pull/2235)).
- Added `PackageKind` and `ProcedureExport` ([#2358](https://github.com/0xMiden/miden-base/pull/2358)).

Expand Down
39 changes: 18 additions & 21 deletions crates/miden-agglayer/asm/bridge/agglayer_faucet.masm
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const P2ID_OUTPUT_NOTE_AMOUNT_MEM_PTR = 611

const ERR_INVALID_CLAIM_PROOF = "invalid claim proof"

#! Inputs: [PROOF_DATA_KEY, LEAF_DATA_KEY]
#! Inputs: [LEAF_DATA_KEY, PROOF_DATA_KEY]
#! Outputs: []
#!
#! Panics if:
Expand All @@ -65,28 +65,23 @@ const ERR_INVALID_CLAIM_PROOF = "invalid claim proof"
#!
#! Invocation: exec
proc validate_claim
# Get bridge_in::check_claim_proof procedure MAST root
procref.bridge_in::check_claim_proof
# => [BRIDGE_PROC_MAST_ROOT]
# get bridge_in::verify_leaf_bridge procedure MAST root
procref.bridge_in::verify_leaf_bridge
# => [BRIDGE_PROC_MAST_ROOT, LEAF_DATA_KEY, PROOF_DATA_KEY]

push.BRIDGE_ID_SLOT[0..2]
# => [bridge_id_idx, BRIDGE_PROC_MAST_ROOT]
# => [bridge_id_idx, BRIDGE_PROC_MAST_ROOT, LEAF_DATA_KEY, PROOF_DATA_KEY]

# Get Bridge AccountId
# get bridge account ID
exec.active_account::get_item
# => [bridge_account_id_prefix, bridge_account_id_suffix, 0, 0, BRIDGE_PROC_MAST_ROOT]
# => [bridge_account_id_prefix, bridge_account_id_suffix, 0, 0, BRIDGE_PROC_MAST_ROOT, LEAF_DATA_KEY, PROOF_DATA_KEY]

movup.2 drop movup.2 drop
# => [bridge_account_id_prefix, bridge_account_id_suffix, BRIDGE_PROC_MAST_ROOT]
# => [bridge_account_id_prefix, bridge_account_id_suffix, BRIDGE_PROC_MAST_ROOT, LEAF_DATA_KEY, PROOF_DATA_KEY]

# Call check_claim_proof procedure on Bridge
# Calling: bridge_in::check_claim_proof
# call bridge_in::verify_leaf_bridge
exec.tx::execute_foreign_procedure
# => [validation_result]

# Assert valid proof data
assert.err=ERR_INVALID_CLAIM_PROOF drop
# => []
# => []
end

# Inputs: []
Expand Down Expand Up @@ -248,20 +243,22 @@ end
pub proc claim
# Check AdviceMap values hash to keys & write CLAIM inputs & DATA_KEYs to global memory
exec.batch_pipe_double_words
# => []
# => [pad(16)]

# VALIDATE CLAIM
mem_loadw_be.LEAF_DATA_KEY_MEM_ADDR padw
mem_loadw_be.PROOF_DATA_KEY_MEM_ADDR
# => [PROOF_DATA_KEY, LEAF_DATA_KEY]
mem_loadw_be.PROOF_DATA_KEY_MEM_ADDR
# => [PROOF_DATA_KEY, pad(12)]
swapw
mem_loadw_be.LEAF_DATA_KEY_MEM_ADDR
# => [LEAF_DATA_KEY, PROOF_DATA_KEY, pad(8)]

# Errors on invalid proof
exec.validate_claim
# => []
# => [pad(16)]

# Create P2ID output note
exec.build_p2id_output_note
# => []
# => [pad(16)]
end

#! Burns the fungible asset from the active note.
Expand Down
177 changes: 153 additions & 24 deletions crates/miden-agglayer/asm/bridge/bridge_in.masm
Original file line number Diff line number Diff line change
@@ -1,45 +1,70 @@
use miden::agglayer::crypto_utils
use miden::core::crypto::hashes::keccak256
use miden::core::mem
use miden::protocol::active_account
use miden::protocol::native_account


# CONSTANTS
# =================================================================================================

const PROOF_DATA_PTR = 0
const PROOF_DATA_WORD_LEN = 134
const SMT_PROOF_LOCAL_EXIT_ROOT_PTR = 0 # local SMT proof is first
const GLOBAL_INDEX_PTR = PROOF_DATA_PTR + 2 * 256 # 512
const EXIT_ROOTS_PTR = GLOBAL_INDEX_PTR + 8 # 520
const MAINNET_EXIT_ROOT_PTR = GLOBAL_INDEX_PTR # it's the first exit root

const GER_UPPER_STORAGE_SLOT=word("miden::agglayer::bridge::ger_upper")
const GER_LOWER_STORAGE_SLOT=word("miden::agglayer::bridge::ger_lower")

# Inputs: [GER_LOWER[4], GER_UPPER[4]]
# Outputs: []
# ERRORS
# =================================================================================================

const ERR_BRIDGE_NOT_MAINNET = "bridge not mainnet"
const ERR_LEADING_BITS_NON_ZERO = "leading bits of global index must be zero"
const ERR_ROLLUP_INDEX_NON_ZERO = "rollup index must be zero for a mainnet deposit"

# PUBLIC INTERFACE
# =================================================================================================

#! Updates the Global Exit Root (GER) in the bridge account storage.
#!
#! Inputs: [GER_LOWER[4], GER_UPPER[4], pad(8)]
#! Outputs: [pad(16)]
#!
#! Invocation: call
pub proc update_ger
push.GER_LOWER_STORAGE_SLOT[0..2]
# => [slot_id_prefix, slot_id_suffix, GER_LOWER[4], GER_UPPER[4]]
# => [slot_id_prefix, slot_id_suffix, GER_LOWER[4], GER_UPPER[4], pad(8)]

exec.native_account::set_item
# => [OLD_VALUE, GER_UPPER[4]]
# => [OLD_VALUE, GER_UPPER[4], pad(8)]

dropw
# => [GER_UPPER[4]]
# => [GER_UPPER[4], pad(12)]

push.GER_UPPER_STORAGE_SLOT[0..2]
# => [slot_id_prefix, slot_id_suffix, GER_UPPER[4]]
# => [slot_id_prefix, slot_id_suffix, GER_UPPER[4], pad(12)]

exec.native_account::set_item
# => [OLD_VALUE]
# => [OLD_VALUE, pad(12)]

dropw
# => []
# => [pad(16)]
end

# Inputs: []
# Output: [GER_ROOT[8]]
pub proc get_rollup_exit_root
# Push dummy GER (8 elements)
push.0.0.0.0.0.0.0.0 # dummy GER
end

#! Checks the validity of the GET proof
#! Computes the leaf value and verifies it against the AggLayer bridge state.
#!
#! Verification is delegated to `verify_leaf` to mimic the AggLayer Solidity contracts.
#! The steps involved in verification are:
#! 1. Compute the GER from the mainnet and rollup exit roots.
#! 2. Assert that the computed GER is valid (exists in storage).
#! 3. Process the global index to determine if it's a mainnet or rollup deposit.
#! 4. Verify the Merkle proof for the provided leaf-index tuple against the computed GER.
#!
#! Inputs:
#! Operand stack: [PROOF_DATA_KEY, LEAF_DATA_KEY, pad(8)]
#! Operand stack: [LEAF_DATA_KEY, PROOF_DATA_KEY, pad(8)]
#! Advice map: {
#! PROOF_DATA_KEY => [
#! smtProofLocalExitRoot[256], // SMT proof for local exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH])
Expand All @@ -60,14 +85,118 @@ end
#! ],
#! }
#!
#! Outputs: [pad(16)]
#!
#! Panics if:
#! - the computed GER is invalid (never injected).
#! - the global index is invalid.
#! - the Merkle proof for the provided leaf-index tuple against the computed GER is invalid.
#!
#! Invocation: call
pub proc check_claim_proof
exec.get_rollup_exit_root
# => [GER_ROOT[8], PROOF_DATA_KEY, LEAF_DATA_KEY]
pub proc verify_leaf_bridge
# get the leaf value. We have all the necessary leaf data in the advice map
exec.crypto_utils::get_leaf_value
# => [LEAF_VALUE[8], PROOF_DATA_KEY, pad(8)]

movupw.3 dropw
# => [LEAF_VALUE[8], PROOF_DATA_KEY, pad(4)]

# delegate proof verification
exec.verify_leaf
# => [pad(16)]
end

# HELPER PROCEDURES
# =================================================================================================

#! Asserts that the provided GER is valid (exists in storage).
#!
#! Inputs: [GER_ROOT[8]]
#! Outputs: []
#!
#! Invocation: exec
proc assert_valid_ger
# TODO verify that GER is in storage
dropw dropw
end

#! Assert the global index is valid for a mainnet deposit.
#!
#! Inputs: [GLOBAL_INDEX[8]]
#! Outputs: [leaf_index]
#!
#! Panics if:
#! - the leading bits of the global index are not zero.
#! - the mainnet flag is not 1.
#! - the rollup index is not 0.
#!
#! Invocation: exec
pub proc process_global_index_mainnet
# for v0.1, let's only implement the mainnet branch
# the top 191 bits of the global index are zero
repeat.5 assertz.err=ERR_LEADING_BITS_NON_ZERO end

# the next element is a u32 mainnet flag bit
# enforce that this limb is one
# => [mainnet_flag, GLOBAL_INDEX[6..8], LEAF_VALUE[8]]
assert.err=ERR_BRIDGE_NOT_MAINNET

# the next element is a u32 rollup index, must be zero for a mainnet deposit
assertz.err=ERR_ROLLUP_INDEX_NON_ZERO

# finally, the leaf index = lowest 32 bits = last limb
# => [leaf_index]
end

#! Verify leaf and checks that it has not been claimed.
#!
#! Inputs:
#! Operand stack: [LEAF_VALUE[8], PROOF_DATA_KEY]
#!
#! Outputs: []
#!
#! Panics if:
#! - the computed GER is invalid (never injected).
#! - the global index is invalid.
#! - the Merkle proof for the provided leaf-index tuple against the computed GER is invalid.
#!
#! Invocation: exec
proc verify_leaf
movupw.2
# load proof data from the advice map into memory
adv.push_mapval
# => [PROOF_DATA_KEY, LEAF_VALUE[8]]

push.SMT_PROOF_LOCAL_EXIT_ROOT_PTR push.PROOF_DATA_WORD_LEN
exec.mem::pipe_preimage_to_memory drop

# 1. compute GER from mainnet + rollup exit roots
push.EXIT_ROOTS_PTR
# => [exit_roots_ptr, LEAF_VALUE[8]]
exec.crypto_utils::compute_ger
# => [GER[8], LEAF_VALUE[8]]

# 2. assert the GER is valid
exec.assert_valid_ger
# => [LEAF_VALUE[8]]

# 3. load global index from memory
padw mem_loadw_le.GLOBAL_INDEX_PTR
padw push.GLOBAL_INDEX_PTR add.4 mem_loadw_le swapw
# => [GLOBAL_INDEX[8], LEAF_VALUE[8]]

# to see if we're dealing with a deposit from mainnet or from a rollup, process the global index
# TODO currently only implemented for mainnet deposits (mainnet flag must be 1)
exec.process_global_index_mainnet
# => [leaf_index]

# Check CLAIM note proof data against current GER
exec.crypto_utils::verify_claim_proof
# => [is_valid_claim_proof]
# load the pointers to the merkle proof and root, to pass to `verify_merkle_proof`
push.SMT_PROOF_LOCAL_EXIT_ROOT_PTR
push.MAINNET_EXIT_ROOT_PTR

swap drop
# => [mainnet_exit_root_ptr, smt_proof_local_exit_root_ptr, leaf_index, LEAF_VALUE[8]]
movdn.10
# => [smt_proof_local_exit_root_ptr, leaf_index, LEAF_VALUE[8], mainnet_exit_root_ptr]
# delegate verification to crypto_utils::verify_merkle_proof (stubbed for now)
exec.crypto_utils::verify_merkle_proof
end
74 changes: 24 additions & 50 deletions crates/miden-agglayer/asm/bridge/crypto_utils.masm
Copy link
Contributor

Choose a reason for hiding this comment

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

Not for this PR, but I wonder if a better name for this module is global_exit_tree.masm. It seems like most (if not all) functionality here is about global exit trees. And the naming would also be consistent with local_exit_tree.masm module we already have.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const LEAF_DATA_START_PTR = 0
#! metadata[8], // ABI encoded metadata (8 felts, fixed size)
#! ],
#! }
#! Outputs: [LEAF_VALUE]
#! Outputs: [LEAF_VALUE[8]]
#!
Comment on lines -22 to 23
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason not to keep the TODO here?

#! Invocation: exec
pub proc get_leaf_value
Expand All @@ -35,59 +35,33 @@ pub proc get_leaf_value

exec.keccak256::hash_bytes
# => [LEAF_VALUE[8]]

# truncate stack
swapdw dropw dropw
# => [LEAF_VALUE[8]]
end

#! Verify leaf and checks that it has not been claimed.
#!
#! This procedure verifies that a claim proof is valid against the Global Exit Tree (GET)
#! and that the leaf has not been previously claimed.
#! Computes the Global Exit Tree (GET) root from the mainnet and rollup exit roots.
#!
#! Inputs:
#! Operand stack: [GER_ROOT[8], PROOF_DATA_KEY, LEAF_DATA_KEY, pad(12)]
#! Advice map: {
#! PROOF_DATA_KEY => [
#! smtProofLocalExitRoot[256], // SMT proof for local exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH])
#! smtProofRollupExitRoot[256], // SMT proof for rollup exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH])
#! globalIndex[8], // Global index (8 felts, uint256 as 8 u32 felts)
#! mainnetExitRoot[8], // Mainnet exit root hash (8 felts, bytes32 as 8 u32 felts)
#! rollupExitRoot[8], // Rollup exit root hash (8 felts, bytes32 as 8 u32 felts)
#! ],
#! LEAF_DATA_KEY => [
#! originNetwork[1], // Origin network identifier (1 felt, uint32)
#! originTokenAddress[5], // Origin token address (5 felts, address as 5 u32 felts)
#! destinationNetwork[1], // Destination network identifier (1 felt, uint32)
#! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts)
#! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts)
#! metadata[8], // ABI encoded metadata (8 felts, fixed size)
#! EMPTY_WORD // padding
#! ],
#! }
#! Outputs:
#! Operand stack: [is_valid]
#! The mainnet exit root is expected at `exit_roots_ptr` and
#! the rollup exit root is expected at `exit_roots_ptr + 8`.
#!
#! Where:
#! - RPO_CLAIM_NOTE_STORAGE_COMMITMENT is the RPO hash commitment of all claim note storage
#! - leafType is the leaf type: [0] transfer Ether / ERC20 tokens, [1] message
#! - originNetwork is the origin network identifier (u32 as Felt)
#! - originAddress is the origin address (5 felts representing address)
#! - destinationNetwork is the destination network identifier (u32 as Felt)
#! - destinationAddress is the destination address (5 felts representing address)
#! - amount is the amount of tokens (u256 as Felt)
#! - metadata is the metadata (4 felts representing 4 u32 0 values)
#! - index is the index of the leaf (u32 as Felt)
#! - claimRoot is the claim root (8 felts representing bytes32)
#! - smtProof is the SMT proof data (570 felts)
#! - is_valid is 1 if the leaf is valid and not claimed, 0 otherwise
#! Inputs: [exit_roots_ptr]
#! Outputs: [GER_ROOT[8]]
#!
#! Invocation: exec
pub proc verify_claim_proof
# TODO: Implement actual Global Exit Tree proof verification

# For now, drop all inputs and return 1 (valid)
dropw dropw dropw dropw
push.1
pub proc compute_ger
push.64 swap
# => [exit_roots_ptr, len_bytes]
exec.keccak256::hash_bytes
# => [GER_ROOT[8]]
end

#! Verifies a Merkle proof for a leaf value against a root.
#!
#! Inputs: [smt_proof_ptr, leaf_index, LEAF_VALUE[8], root_ptr]
#! Outputs: []
#!
pub proc verify_merkle_proof
Comment on lines +56 to +61
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm assuming this procedure will panic if the proof cannot be verified, right? If so, would be good to say this explicitly.

Also, I'm assuming we are panicking rather than returning a boolean too keep the interfaces consistent with Solidity code?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the proc here is just a stub, and the panic docs are already addressed by Andrew in #2361

Also, I'm assuming we are panicking rather than returning a boolean too keep the interfaces consistent with Solidity code?

indeed

# TODO pending https://github.com/0xMiden/miden-base/issues/2278
drop
drop
dropw dropw
drop
end
Loading