Skip to content
Closed
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
107 changes: 107 additions & 0 deletions crates/miden-lib/asm/agglayer/account_components/agglayer_faucet.masm
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use.agglayer::bridge_out
use.miden::active_account
use.miden::active_note
use.miden::contracts::faucets::network_fungible
use.miden::native_account
use.miden::note
use.miden::output_note
use.miden::tx
use.std::sys

const.BRIDGE_ID_IDX=word("miden::agglayer::faucet")

#! Custom agglayer faucet component that combines network faucet functionality
#! with bridge validation via Foreign Procedure Invocation (FPI).
#!
#! This component provides a "claim" procedure that:
#! 1. Extracts CLAIM note details
#! 2. Validates the claim against a bridge MMR account via FPI
#! 3. If validation passes, mints assets using network faucet functionality

#! Validates a CLAIM note against the bridge MMR account via FPI.
#!
#! This procedure performs a foreign procedure invocation to the bridge MMR account
#! to validate that the CLAIM note details are consistent with the MMR state.
#!
#! Inputs: []
#! Outputs: []
#!
#! Where:
#! - bridge_account_id_prefix, bridge_account_id_suffix: ID of the bridge MMR account
#! - ASSET: The asset being claimed
#! - validation_result: 1 if valid, 0 if invalid
#!
#! Invocation: exec
proc validate_claim_via_fpi

# get check_claim_proof root
procref.bridge_out::check_claim_proof
# => [MAST_ROOT]

push.BRIDGE_ID_IDX[0..2]
# => [bridge_id_idx, MAST_ROOT]

# get bridge account id
exec.active_account::get_item
# => [bridge_account_id_prefix, bridge_account_id_suffix, 0, 0, MAST_ROOT]

movup.2 drop movup.2 drop
# => [bridge_account_id_prefix, bridge_account_id_suffix, MAST_ROOT]

# call check_claim_proof proc on bridge
exec.tx::execute_foreign_procedure
# => [validation_result]

push.333 debug.stack drop

assert.err="invalid proof" drop
# => []
end

#! Main claim procedure that processes CLAIM notes.
#!
#! This procedure:
#! 1. Extracts the asset and bridge details from the CLAIM note
#! 2. Validates the claim against the bridge MMR account via FPI
#! 3. If validation passes, creates a MINT note for the asset
#!
#! Inputs: []
#! Outputs: []
#!
#! Invocation: call
export.claim
# Get the CLAIM note assets

# validate CLAIM note proof
# panics if invalid
exec.validate_claim_via_fpi
# => []

# TODO: output note
# exec.distribute
end

#! Distributes fungible assets by creating MINT notes.
#!
#! This procedure wraps the network faucet's distribute functionality
#! and can be called directly for standard minting operations.
#!
#! Inputs: [amount, tag, aux, note_type, execution_hint, RECIPIENT]
#! Outputs: [note_idx]
#!
#! Invocation: call
export.distribute
call.network_fungible::distribute
end

#! Burns fungible assets.
#!
#! This procedure wraps the network faucet's burn functionality.
#!
#! Inputs: [ASSET]
#! Outputs: [ASSET]
#!
#! Invocation: call
export.burn
call.network_fungible::burn
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

pub proc placeholder
nop
end
15 changes: 15 additions & 0 deletions crates/miden-lib/asm/agglayer/account_components/bridge_out.masm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ const.AUX=0
const.NUM_BURN_NOTE_INPUTS=0
const.BURN_ASSET_MEM_PTR=24

#! Checks the validity of the GET proof
#!
#! Inputs: []
#! Outputs: [is_valid_claim_proof]
#!
#! Invocation: exec
pub proc check_claim_proof
exec.local_exit_tree::verify_claim_proof
# => [is_valid_claim_proof]

# truncate stack
swap drop
# => [is_valid_claim_proof]
end

#! Computes the SERIAL_NUM of the outputted BURN note.
#!
#! The serial number is computed as hash(B2AGG_SERIAL_NUM, ASSET).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,10 @@ pub proc add_asset_message

exec.write_mmr_frontier_root
# => []
end

#! Verifies the validity of the CLAIM bridge asset in proof
#! stubbed out
pub proc verify_claim_proof
push.1
end
12 changes: 12 additions & 0 deletions crates/miden-lib/asm/agglayer/note_scripts/CLAIM.masm
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use.agglayer::agglayer_faucet -> agg_faucet

begin
dropw
# => [pad(16)]

# => []
push.111 debug.stack drop

call.agg_faucet::claim

end
3 changes: 2 additions & 1 deletion crates/miden-lib/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ fn main() -> Result<()> {

// compile transaction kernel
let mut assembler =
compile_tx_kernel(&source_dir.join(ASM_TX_KERNEL_DIR), &target_dir.join("kernels"))?;
compile_tx_kernel(&source_dir.join(ASM_TX_KERNEL_DIR), &target_dir.join("kernels"))?
.with_debug_mode(true);

// compile miden library
let miden_lib = compile_miden_lib(&source_dir, &target_dir, assembler.clone())?;
Expand Down
66 changes: 66 additions & 0 deletions crates/miden-lib/src/agglayer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ pub fn b2agg_script() -> NoteScript {
B2AGG_SCRIPT.clone()
}

// Initialize the CLAIM note script only once
static CLAIM_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let bytes =
include_bytes!(concat!(env!("OUT_DIR"), "/assets/agglayer/note_scripts/CLAIM.masb"));
let program = Program::read_from_bytes(bytes).expect("Shipped CLAIM script is well-formed");
NoteScript::new(program)
});

/// Returns the CLAIM (Bridge from AggLayer) note script.
pub fn claim_script() -> NoteScript {
CLAIM_SCRIPT.clone()
}

// AGGLAYER ACCOUNT COMPONENTS
// ================================================================================================

Expand Down Expand Up @@ -79,6 +92,59 @@ pub fn bridge_out_component(storage_slots: Vec<StorageSlot>) -> AccountComponent
.with_supports_all_types()
}

// Initialize the Bridge In library only once
static BRIDGE_IN_LIBRARY: LazyLock<Library> = LazyLock::new(|| {
let bytes = include_bytes!(concat!(
env!("OUT_DIR"),
"/assets/agglayer/account_components/bridge_in.masl"
));
Library::read_from_bytes(bytes).expect("Shipped Bridge In library is well-formed")
});

/// Returns the Bridge In Library.
pub fn bridge_in_library() -> Library {
BRIDGE_IN_LIBRARY.clone()
}

/// Creates a Bridge In component with the specified storage slots.
///
/// This component uses the bridge_in library and can be added to accounts
/// that need to bridge assets in from the AggLayer.
pub fn bridge_in_component(storage_slots: Vec<StorageSlot>) -> AccountComponent {
let library = bridge_in_library();

AccountComponent::new(library, storage_slots)
.expect("bridge_in component should satisfy the requirements of a valid account component")
.with_supports_all_types()
}

// Initialize the Agglayer Faucet library only once
static AGGLAYER_FAUCET_LIBRARY: LazyLock<Library> = LazyLock::new(|| {
let bytes = include_bytes!(concat!(
env!("OUT_DIR"),
"/assets/agglayer/account_components/agglayer_faucet.masl"
));
Library::read_from_bytes(bytes).expect("Shipped Agglayer Faucet library is well-formed")
});

/// Returns the Agglayer Faucet Library.
pub fn agglayer_faucet_library() -> Library {
AGGLAYER_FAUCET_LIBRARY.clone()
}

/// 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.
pub fn agglayer_faucet_component(storage_slots: Vec<StorageSlot>) -> AccountComponent {
let library = agglayer_faucet_library();

AccountComponent::new(library, storage_slots)
.expect("agglayer_faucet component should satisfy the requirements of a valid account component")
.with_supports_all_types()
}

/// Creates a combined Bridge Out component that includes both bridge_out and local_exit_tree
/// modules.
///
Expand Down
5 changes: 2 additions & 3 deletions crates/miden-testing/src/mock_chain/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -714,9 +714,8 @@ impl MockChain {

/// Gets foreign account inputs to execute FPI transactions.
///
/// Only used internally and so does not need to be public.
#[cfg(test)]
pub(crate) fn get_foreign_account_inputs(
/// Used in tests to get foreign account inputs for FPI calls.
pub fn get_foreign_account_inputs(
&self,
account_id: AccountId,
) -> anyhow::Result<(Account, AccountWitness)> {
Expand Down
129 changes: 129 additions & 0 deletions crates/miden-testing/tests/agglayer/bridge_in.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
extern crate alloc;

use miden_lib::account::wallets::BasicWallet;
use miden_lib::agglayer::{agglayer_faucet_component, bridge_out_component, claim_script};
use miden_lib::note::WellKnownNote;
use miden_objects::account::{
Account,
AccountId,
AccountStorageMode,
StorageSlot,
StorageSlotName,
};
use miden_objects::note::{
Note,
NoteAssets,
NoteExecutionHint,
NoteInputs,
NoteMetadata,
NoteRecipient,
NoteScript,
NoteTag,
NoteType,
};
use miden_objects::{Felt, Word};
use miden_testing::{AccountState, Auth, MockChain};
use rand::Rng;

/// Tests the bridge-in flow: CLAIM note -> Aggfaucet (FPI to Bridge) -> P2ID note created.
#[tokio::test]
async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> {
let mut builder = MockChain::builder();

// CREATE BRIDGE ACCOUNT (with bridge_out component for MMR validation)
// --------------------------------------------------------------------------------------------
let bridge_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge").unwrap();
let bridge_storage_slots = vec![StorageSlot::with_empty_map(bridge_storage_slot_name)];
let bridge_component = bridge_out_component(bridge_storage_slots);
let bridge_account_builder = Account::builder(builder.rng_mut().random())
.storage_mode(AccountStorageMode::Public)
.with_component(bridge_component);
let bridge_account = builder.add_account_from_builder(
Auth::IncrNonce,
bridge_account_builder,
AccountState::Exists,
)?;

println!(
"bridge account id: {} {}",
bridge_account.id().prefix().as_felt(),
bridge_account.id().suffix()
);

let test_parse =
AccountId::try_from([bridge_account.id().prefix().as_felt(), bridge_account.id().suffix()])
.unwrap();
assert_eq!(test_parse, bridge_account.id());

// CREATE AGGLAYER FAUCET ACCOUNT (with agglayer_faucet component)
// --------------------------------------------------------------------------------------------
let bridge_account_id_word = Word::new([
Felt::new(0),
Felt::new(0),
bridge_account.id().suffix(),
bridge_account.id().prefix().as_felt(),
]);
let agglayer_storage_slot_name = StorageSlotName::new("miden::agglayer::faucet").unwrap();
let agglayer_storage_slots =
vec![StorageSlot::with_value(agglayer_storage_slot_name, bridge_account_id_word)];
let agglayer_component = agglayer_faucet_component(agglayer_storage_slots);
let agglayer_faucet_builder = Account::builder(builder.rng_mut().random())
.storage_mode(AccountStorageMode::Public)
.with_component(agglayer_component);
let agglayer_faucet = builder.add_account_from_builder(
Auth::IncrNonce,
agglayer_faucet_builder,
AccountState::Exists,
)?;

// CREATE USER ACCOUNT TO RECEIVE P2ID NOTE
// --------------------------------------------------------------------------------------------
let user_account_builder =
Account::builder(builder.rng_mut().random()).with_component(BasicWallet);
let _user_account = builder.add_account_from_builder(
Auth::IncrNonce,
user_account_builder,
AccountState::Exists,
)?;

// BUILD MOCK CHAIN WITH ALL ACCOUNTS
// --------------------------------------------------------------------------------------------
let mut mock_chain = builder.build()?;
mock_chain.prove_next_block()?;

// CREATE CLAIM NOTE WITH BRIDGE METADATA
// --------------------------------------------------------------------------------------------
let tag = NoteTag::for_local_use_case(0, 0).unwrap();
let aux = Felt::new(0);
let note_execution_hint = NoteExecutionHint::always();
let note_type = NoteType::Public;

let claim_script = claim_script();

let inputs = NoteInputs::new(vec![])?;
let claim_note_metadata =
NoteMetadata::new(agglayer_faucet.id(), note_type, tag, note_execution_hint, aux)?;
let claim_note_assets = NoteAssets::new(vec![])?; // Empty assets - will be validated and minted
let serial_num = Word::from([1, 2, 3, 4u32]);
let claim_note_recipient = NoteRecipient::new(serial_num, claim_script, inputs);
let claim_note = Note::new(claim_note_assets, claim_note_metadata, claim_note_recipient);

// EXECUTE CLAIM NOTE AGAINST AGGLAYER FAUCET (with FPI to Bridge)
// --------------------------------------------------------------------------------------------

let p2id_note_script: NoteScript = WellKnownNote::P2ID.script();
let foreign_account_inputs = mock_chain.get_foreign_account_inputs(bridge_account.id())?;

let tx_context = mock_chain
.build_tx_context(agglayer_faucet.id(), &[], &[claim_note])?
.add_note_script(p2id_note_script.clone())
.foreign_accounts(vec![foreign_account_inputs])
.build()?;

let _executed_transaction = tx_context.execute().await?;

// VERIFY P2ID NOTE WAS CREATED
// --------------------------------------------------------------------------------------------

Ok(())
}
1 change: 1 addition & 0 deletions crates/miden-testing/tests/agglayer/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
mod bridge_in;
pub mod asset_conversion;
mod bridge_out;
Loading
Loading