Add Agglayer CLAIM note & bridging in functionality#2188
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces functionality for bridging assets into Miden from the AggLayer by implementing a CLAIM note mechanism. Users can create CLAIM notes that are consumed by an AggLayer faucet, which validates the claim through a Foreign Procedure Invocation (FPI) to the bridge account before minting assets.
Key Changes:
- Added
CLAIMnote script and creation helper function - Implemented
agglayer_faucetcomponent that validates claims via FPI to bridge account - Added test coverage for the complete bridge-in flow from
CLAIMnote toP2IDnote creation
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
crates/miden-testing/tests/agglayer/mod.rs |
Adds bridge_in module to test suite |
crates/miden-testing/tests/agglayer/bridge_in.rs |
New integration test validating the complete CLAIM → P2ID flow |
crates/miden-testing/src/mock_chain/chain.rs |
Makes get_foreign_account_inputs public for FPI testing |
crates/miden-lib/src/errors/note_script_errors.rs |
Adds error constant for CLAIM note input validation |
crates/miden-lib/src/agglayer/mod.rs |
Adds CLAIM script, agglayer_faucet component, and note creation helper |
crates/miden-lib/asm/agglayer/note_scripts/CLAIM.masm |
CLAIM note script implementation |
crates/miden-lib/asm/agglayer/account_components/local_exit_tree.masm |
Adds stubbed verify_claim_proof procedure |
crates/miden-lib/asm/agglayer/account_components/bridge_in.masm |
Bridge-in component for claim proof validation |
crates/miden-lib/asm/agglayer/account_components/agglayer_faucet.masm |
AggLayer faucet component with FPI validation and asset distribution |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
crates/miden-lib/asm/agglayer/account_components/local_exit_tree.masm
Outdated
Show resolved
Hide resolved
crates/miden-lib/asm/agglayer/account_components/local_exit_tree.masm
Outdated
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ee.masm Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Did you mean "how to pass these values to the (most of) These values will be used for merkle path verification. So let's say we have the 64 Words from the note inputs, and we fetched the relevant GER from the So, inside the
I would either:
But since figuring out (a) is going to take some time probably, let's just skip it for now - since anyway they would be mocked inside |
crates/miden-lib/asm/agglayer/account_components/agglayer_faucet.masm
Outdated
Show resolved
Hide resolved
crates/miden-lib/asm/agglayer/account_components/agglayer_faucet.masm
Outdated
Show resolved
Hide resolved
crates/miden-lib/asm/agglayer/account_components/agglayer_faucet.masm
Outdated
Show resolved
Hide resolved
crates/miden-lib/asm/agglayer/account_components/agglayer_faucet.masm
Outdated
Show resolved
Hide resolved
crates/miden-lib/asm/agglayer/account_components/agglayer_faucet.masm
Outdated
Show resolved
Hide resolved
…et.masm Co-authored-by: Marti <marti@miden.team>
…et.masm Co-authored-by: Marti <marti@miden.team>
…et.masm Co-authored-by: Marti <marti@miden.team>
…et.masm Co-authored-by: Marti <marti@miden.team>
…et.masm Co-authored-by: Marti <marti@miden.team>
…et.masm Co-authored-by: Marti <marti@miden.team>
Thank you! A few questions/comments on this (some of which covered offline):
Regarding proofs, I'm assuming the following:
Is this correct? And if so, this again seems like |
|
As per @bobbinth 's suggestion I updated the Then in the CLAIM note we call Later on as a follow up, the When implementing the claim asset flow, I closely followed the logic of the Agglayer |
|
To answer your questions @bobbinth Question 1
It’s a packed “locator” for the leaf being claimed (not the GER). Layout is:
Question 2
Both.
https://docs.agglayer.dev/agglayer/core-concepts/unified-bridge/data-structures/ |
mmagician
left a comment
There was a problem hiding this comment.
Not a full review yet, but the new structure looks very clean! Great work 💯
Co-authored-by: Marti <marti@miden.team>
Co-authored-by: Marti <marti@miden.team>
Co-authored-by: Marti <marti@miden.team>
Co-authored-by: Marti <marti@miden.team>
bobbinth
left a comment
There was a problem hiding this comment.
Looks good! Thank you! I left a bunch of comments inline - but many of them are for follow-ups. There are a few that could be good to address in this PR though. After this, we are good to merge.
| // AGGLAYER ACCOUNT COMPONENTS | ||
| // ================================================================================================ |
There was a problem hiding this comment.
Not for this PR, but I still think we should have much fewer components. Specifically, I think we need just two account components: AggLayerBridge and AggLayerFaucet.
There was a problem hiding this comment.
I think the components are a good way to organize the code into smaller chunks. Especially parts like bridging-in vs. bridging-out, but also local exit tree, make a lot of sense to me.
Are there any issues with having multiple components that get merged into a single account code? (I think this was generally the idea of introducing AccountComponents)
There was a problem hiding this comment.
I think the components are a good way to organize the code into smaller chunks.
I think we already get this by breaking the code up into multiple modules - i.e.., bridging out code is one module, Kecck MMR in another. Introducing a layer of components on top of this is an overkill IMO.
Are there any issues with having multiple components that get merged into a single account code? (I think this was generally the idea of introducing
AccountComponents)
No specific issues aside from making the code more complicated, IMO. The general idea around components is that these are relatively self contained and we can add multiple self-contained components to a given account. In this case, we get a set of inter-dependent components which all need to be added to the account together to make it work correctly. In my mind, this means that all of these should be a single component.
Basically, in my mind, we should be able to deploy a component in an account regardless of other components being there. There, of course, will be exceptions to this, but most usually, that should be the case.
There was a problem hiding this comment.
Ok, makes sense, thank you for the explanation 👍🏼
| /// | ||
| /// # Panics | ||
| /// Panics if the input is not exactly 32 bytes | ||
| pub fn bytes32_to_felts(bytes32: &[u8; 32]) -> Vec<Felt> { |
There was a problem hiding this comment.
I don't think we need this function. Instead, we should be able to use bytes_to_packed_u32_elements() from miden_core::utils module.
There was a problem hiding this comment.
Afaik, miden_core::utils::packed_u32_felts_to_bytes is little endian, and for Ethereum/Agglayer compatibility we need to be big endian
There was a problem hiding this comment.
I thought Ethereum/AggLayer was little-endian as well. At least I remember we did the amount conversions in little-endian because of this (e.g., here). What Ethereum/AggLayer data is in big-endian form?
There was a problem hiding this comment.
Ah you’re right, I got confused here. Ethereum/AggLayer bytes32 is just 32 raw bytes (Solidity prints them in order).
So I agree we probably don’t need bytes32_to_felts; miden_core::utils::bytes_to_packed_u32_elements() should work.
To understand how this all works concretely I wrote an example that hashes a value in Solidity and in the Miden VM.
Rust test:
fn u32_words_to_solidity_bytes32_hex(words: &[u64]) -> String {
assert_eq!(words.len(), 8, "expected 8 u32 words = 32 bytes");
let mut out = [0u8; 32];
for (i, &w) in words.iter().enumerate() {
let le = (w as u32).to_le_bytes();
out[i * 4..i * 4 + 4].copy_from_slice(&le);
}
let mut s = String::from("0x");
for b in out {
s.push_str(&format!("{:02x}", b));
}
s
}
#[test]
fn test_keccak_hash_bytes_test() {
let mut input_u8: Vec<u8> = vec![0u8; 24];
input_u8.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
let len_bytes = input_u8.len();
let preimage = KeccakPreimage::new(input_u8.to_vec());
let input_felts = preimage.as_felts();
let memory_stores_source = masm_store_felts(&input_felts, INPUT_MEMORY_ADDR);
let source = format!(
r#"
use miden::core::sys
use miden::core::crypto::hashes::keccak256
begin
# Store packed u32 values in memory
{memory_stores_source}
# Push wrapper inputs
push.{len_bytes}.{INPUT_MEMORY_ADDR}
# => [ptr, len_bytes]
exec.keccak256::hash_bytes
# => [DIGEST_U32[8]]
exec.sys::truncate_stack
end
"#,
);
let test = build_debug_test!(source, &[]);
let digest: Vec<u64> = preimage.digest().as_ref().iter().map(Felt::as_int).collect();
test.expect_stack(&digest);
let digest_words: Vec<u64> = preimage.digest().as_ref().iter().map(Felt::as_int).collect();
let solidity_hex = u32_words_to_solidity_bytes32_hex(&digest_words);
println!("solidity-style digest: {solidity_hex}");
println!("digest: {:?}", digest);
}Output:
solidity-style digest: 0x514148c05833ddeea0364a81300262a25ad938a9a4d00acb82fbc1f0a17b1bcf
digest: [3225960785, 4007474008, 2169124512, 2724332080, 2839075162, 3406483620, 4039244674, 3474684833]
Solidity example:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract KeccakU256 {
function hashUint256(uint256 x) external pure returns (bytes32) {
return keccak256(abi.encodePacked(x));
}
// Fixed test vector matching your Rust bytes:
// 0x00..00 0102030405060708 (32 bytes total)
function hashFixed() external pure returns (bytes32) {
uint256 x = 0x0102030405060708;
return keccak256(abi.encodePacked(x));
}
}Output when calling hashFixed():
bytes32: 0x514148c05833ddeea0364a81300262a25ad938a9a4d00acb82fbc1f0a17b1bcf
There was a problem hiding this comment.
Only thing is bytes_to_packed_u32_elements is not available in miden-vm v0.20.1?
| /// | ||
| /// # Errors | ||
| /// Returns an error if the vector doesn't contain exactly 8 felts | ||
| pub fn felts_to_bytes32(felts: &[Felt]) -> Result<[u8; 32], String> { |
There was a problem hiding this comment.
Similar comment as above, but about packed_u32_elements_to_bytes() function from miden_core::utils.
There was a problem hiding this comment.
I could not find the packed_u32_elements_to_bytes function in miden_core::utils
There was a problem hiding this comment.
Ah yes - both packed_u32_elements_to_bytes and bytes_to_packed_u32_elements are currently on next in miden-vm. Could we make an issue to replace these with the miden-vm versions once we move to the next VM?
I closed #2119 as the name of the branch
ajl-claim-to-mint-notewas misleading and no longer matched the design we agreed upon.Description
This PR adds a stubbed out version of the
CLAIMnote which enables minting of an asset from the Agglayer faucet contract.High level overview of the flow:
User creates
CLAIMnetwork note, which is consumed by the agglayer faucet contract. The agglayer faucet contract does an FPI call to the agglayer bridge which checks the validity of the rollup exit root Merkle Proof (currently stubbed out). If the merkle proof is valid, the bridge returns a "true" boolean which allows the user to mint the specified output note & asset, if false, the agglayer faucet will panic.High level flow:
CLAIM->agglayer_faucet::claim-> FPI call to Bridge ->agglayer_faucet::distribute->P2IDnoteCurrently the
CLAIMnote does not encode any merkle proof data as outlined here: #1910 (comment)There are a couple questions:
If note inputs is ~600
Feltvalues, how to pass these values to theagglayer_faucetvia.call? Do I need to push theFeltvalues to theAdviceMapin theCLAIMnote, and then in theagglayer_bridgeduring the FPI call, read these values?Does this initial iteration of the
CLAIMnote need to stub out adding these ~600Feltvalues asNoteInputs?