diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c48cb95db..f267b36208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)). +- Added B2AGG and UPDATE_GER note attachment target checks ([#2334](https://github.com/0xMiden/miden-base/pull/2334)). - Added double-word array data structure abstraction over storage maps ([#2299](https://github.com/0xMiden/miden-base/pull/2299)). - 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)). diff --git a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm index a62e213daa..9523160e9c 100644 --- a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm +++ b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm @@ -2,6 +2,8 @@ use miden::agglayer::bridge_out use miden::protocol::account_id use miden::protocol::active_account use miden::protocol::active_note +use miden::protocol::note +use miden::standards::attachments::network_account_target use miden::standards::wallets::basic->basic_wallet # CONSTANTS @@ -12,16 +14,18 @@ const B2AGG_NOTE_NUM_STORAGE_ITEMS=6 # ERRORS # ================================================================================================= const ERR_B2AGG_WRONG_NUMBER_OF_ASSETS="B2AGG script requires exactly 1 note asset" - const ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS="B2AGG script expects exactly 6 note storage items" +const ERR_B2AGG_TARGET_ACCOUNT_MISMATCH="B2AGG note attachment target account does not match consuming account" + +# NOTE SCRIPT +# ================================================================================================= #! Bridge-to-AggLayer (B2AGG) note script: bridges assets from Miden to an AggLayer-connected chain. #! #! This note can be consumed in two ways: #! - If the consuming account is the sender (reclaim): the note's assets are added back to the consuming account. -#! - If the consuming account is the Agglayer Bridge: the note's assets are moved to a BURN note, +#! - If the consuming account is the Agglayer Bridge: the note's assets are moved to a BURN note, #! and the note details are hashed into a leaf and appended to the Local Exit Tree. -#! global exit root (GER) merkle tree structure. #! #! Inputs: [] #! Outputs: [] @@ -34,10 +38,13 @@ const ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS="B2AGG script expects exactly #! - destination_address_2: bytes 8-11 #! - destination_address_3: bytes 12-15 #! - destination_address_4: bytes 16-19 +#! Note attachment is constructed from a NetworkAccountTarget standard: +#! - [0, exec_hint_tag, target_id_prefix, target_id_suffix] #! #! Panics if: #! - The note does not contain exactly 6 storage items. #! - The note does not contain exactly 1 asset. +#! - The note attachment does not target the consuming account. #! begin dropw @@ -58,6 +65,11 @@ begin exec.basic_wallet::add_assets_to_account # => [pad(16)] else + # Ensure note attachment targets the consuming bridge account. + exec.network_account_target::active_account_matches_target_account + assert.err=ERR_B2AGG_TARGET_ACCOUNT_MISMATCH + # => [pad(16)] + # Store note storage -> mem[8..14] push.8 exec.active_note::get_storage # => [num_storage_items, dest_ptr, pad(16)] diff --git a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm index 0e4ddd8161..1ca3d1ab9d 100644 --- a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm +++ b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm @@ -1,5 +1,9 @@ use miden::agglayer::bridge_in use miden::protocol::active_note +use miden::protocol::active_account +use miden::protocol::account_id +use miden::protocol::note +use miden::standards::attachments::network_account_target # CONSTANTS # ================================================================================================= @@ -10,6 +14,10 @@ const STORAGE_PTR_GER_UPPER = 4 # ERRORS # ================================================================================================= const ERR_UPDATE_GER_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "UPDATE_GER script expects exactly 8 note storage items" +const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH = "UPDATE_GER note attachment target account does not match consuming account" + +# NOTE SCRIPT +# ================================================================================================= #! Agglayer Bridge UPDATE_GER script: updates the GER by calling the bridge_in::update_ger function. #! @@ -33,6 +41,13 @@ begin dropw # => [pad(16)] + # Ensure note attachment targets the consuming bridge account. + exec.network_account_target::active_account_matches_target_account + assert.err=ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH + # => [pad(16)] + + # proceed with the GER update logic + push.STORAGE_PTR_GER_LOWER exec.active_note::get_storage # => [num_storage_items, dest_ptr, pad(16)] @@ -49,4 +64,4 @@ begin call.bridge_in::update_ger # => [pad(16)] -end \ No newline at end of file +end diff --git a/crates/miden-agglayer/src/b2agg_note.rs b/crates/miden-agglayer/src/b2agg_note.rs new file mode 100644 index 0000000000..49451976ab --- /dev/null +++ b/crates/miden-agglayer/src/b2agg_note.rs @@ -0,0 +1,131 @@ +//! Bridge Out note creation utilities. +//! +//! This module provides helpers for creating B2AGG (Bridge to AggLayer) notes, +//! which are used to bridge assets out from Miden to the AggLayer network. + +use alloc::string::ToString; +use alloc::vec::Vec; + +use miden_assembly::utils::Deserializable; +use miden_core::{Felt, Program, Word}; +use miden_protocol::account::AccountId; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{ + Note, + NoteAssets, + NoteAttachment, + NoteExecutionHint, + NoteMetadata, + NoteRecipient, + NoteScript, + NoteStorage, + NoteType, +}; +use miden_standards::note::NetworkAccountTarget; +use miden_utils_sync::LazyLock; + +use crate::EthAddressFormat; + +// NOTE SCRIPT +// ================================================================================================ + +// Initialize the B2AGG note script only once +static B2AGG_SCRIPT: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/B2AGG.masb")); + let program = Program::read_from_bytes(bytes).expect("Shipped B2AGG script is well-formed"); + NoteScript::new(program) +}); + +// B2AGG NOTE +// ================================================================================================ + +/// B2AGG (Bridge to AggLayer) note. +/// +/// This note is used to bridge assets from Miden to another network via the AggLayer. +/// When consumed by a bridge account, the assets are burned and a corresponding +/// claim can be made on the destination network. B2AGG notes are always public. +pub struct B2AggNote; + +impl B2AggNote { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// Expected number of storage items for a B2AGG note. + pub const NUM_STORAGE_ITEMS: usize = 6; + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the B2AGG (Bridge to AggLayer) note script. + pub fn script() -> NoteScript { + B2AGG_SCRIPT.clone() + } + + /// Returns the B2AGG note script root. + pub fn script_root() -> Word { + B2AGG_SCRIPT.root() + } + + // BUILDERS + // -------------------------------------------------------------------------------------------- + + /// Creates a B2AGG (Bridge to AggLayer) note. + /// + /// This note is used to bridge assets from Miden to another network via the AggLayer. + /// When consumed by a bridge account, the assets are burned and a corresponding + /// claim can be made on the destination network. B2AGG notes are always public. + /// + /// # Parameters + /// - `destination_network`: The AggLayer-assigned network ID for the destination chain + /// - `destination_address`: The Ethereum address on the destination network + /// - `assets`: The assets to bridge (must be fungible assets from a network faucet) + /// - `target_account_id`: The account ID that will consume this note (bridge account) + /// - `sender_account_id`: The account ID of the note creator + /// - `rng`: Random number generator for creating the note serial number + /// + /// # Errors + /// Returns an error if note creation fails. + pub fn create( + destination_network: u32, + destination_address: EthAddressFormat, + assets: NoteAssets, + target_account_id: AccountId, + sender_account_id: AccountId, + rng: &mut R, + ) -> Result { + let note_storage = build_note_storage(destination_network, destination_address)?; + + let attachment = NoteAttachment::from( + NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always) + .map_err(|e| NoteError::other(e.to_string()))?, + ); + + let metadata = + NoteMetadata::new(sender_account_id, NoteType::Public).with_attachment(attachment); + + let recipient = NoteRecipient::new(rng.draw_word(), Self::script(), note_storage); + + Ok(Note::new(assets, metadata, recipient)) + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Builds the note storage for a B2AGG note. +/// +/// The storage layout is: +/// - 1 felt: destination_network +/// - 5 felts: destination_address (20 bytes as 5 u32 values) +fn build_note_storage( + destination_network: u32, + destination_address: EthAddressFormat, +) -> Result { + let mut elements = Vec::with_capacity(6); + + elements.push(Felt::new(destination_network as u64)); + elements.extend(destination_address.to_elements()); + + NoteStorage::new(elements) +} diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index 308e40e3db..a1874001d9 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -12,6 +12,8 @@ use miden_protocol::errors::MasmError; /// Error Message: "most-significant 4 bytes (addr4) must be zero" pub const ERR_ADDR4_NONZERO: MasmError = MasmError::from_static_str("most-significant 4 bytes (addr4) must be zero"); +/// Error Message: "B2AGG note attachment target account does not match consuming account" +pub const ERR_B2AGG_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("B2AGG note attachment target account does not match consuming account"); /// Error Message: "B2AGG script expects exactly 6 note storage items" pub const ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::from_static_str("B2AGG script expects exactly 6 note storage items"); /// Error Message: "B2AGG script requires exactly 1 note asset" @@ -47,5 +49,7 @@ pub const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT: MasmError = MasmError::from_static_st /// Error Message: "merkle proof verification failed: provided SMT root does not match the computed root" pub const ERR_SMT_ROOT_VERIFICATION_FAILED: MasmError = MasmError::from_static_str("merkle proof verification failed: provided SMT root does not match the computed root"); +/// Error Message: "UPDATE_GER note attachment target account does not match consuming account" +pub const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("UPDATE_GER note attachment target account does not match consuming account"); /// Error Message: "UPDATE_GER script expects exactly 8 note storage items" pub const ERR_UPDATE_GER_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::from_static_str("UPDATE_GER script expects exactly 8 note storage items"); diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 82ee38d116..d1538716e7 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -24,12 +24,14 @@ use miden_standards::account::auth::NoAuth; use miden_standards::account::faucets::NetworkFungibleFaucet; use miden_utils_sync::LazyLock; +pub mod b2agg_note; pub mod claim_note; pub mod errors; pub mod eth_types; pub mod update_ger_note; pub mod utils; +pub use b2agg_note::B2AggNote; pub use claim_note::{ ClaimNoteStorage, ExitRoot, @@ -40,22 +42,11 @@ pub use claim_note::{ create_claim_note, }; pub use eth_types::{EthAddressFormat, EthAmount, EthAmountError}; -pub use update_ger_note::create_update_ger_note; +pub use update_ger_note::UpdateGerNote; // AGGLAYER NOTE SCRIPTS // ================================================================================================ -// Initialize the B2AGG note script only once -static B2AGG_SCRIPT: LazyLock = LazyLock::new(|| { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/B2AGG.masb")); - Program::read_from_bytes(bytes).expect("Shipped B2AGG script is well-formed") -}); - -/// Returns the B2AGG (Bridge to AggLayer) note script. -pub fn b2agg_script() -> Program { - B2AGG_SCRIPT.clone() -} - // Initialize the CLAIM note script only once static CLAIM_SCRIPT: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/CLAIM.masb")); @@ -68,19 +59,6 @@ pub fn claim_script() -> NoteScript { CLAIM_SCRIPT.clone() } -// Initialize the UPDATE_GER note script only once -static UPDATE_GER_SCRIPT: LazyLock = LazyLock::new(|| { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/UPDATE_GER.masb")); - let program = - Program::read_from_bytes(bytes).expect("Shipped UPDATE_GER script is well-formed"); - NoteScript::new(program) -}); - -/// Returns the UPDATE_GER note script. -pub fn update_ger_script() -> NoteScript { - UPDATE_GER_SCRIPT.clone() -} - // AGGLAYER ACCOUNT COMPONENTS // ================================================================================================ @@ -268,22 +246,28 @@ pub fn create_agglayer_faucet_component( /// Creates a complete bridge account builder with the standard configuration. pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder { + // Create the "bridge_in" component let ger_upper_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger_upper") .expect("Bridge storage slot name should be valid"); let ger_lower_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger_lower") .expect("Bridge storage slot name should be valid"); - let bridge_storage_slots = vec![ + let bridge_in_storage_slots = vec![ StorageSlot::with_value(ger_upper_storage_slot_name, Word::empty()), StorageSlot::with_value(ger_lower_storage_slot_name, Word::empty()), ]; - let bridge_in_comp = bridge_in_component(bridge_storage_slots); - let bridge_out_comp = bridge_out_component(vec![]); + let bridge_in_component = bridge_in_component(bridge_in_storage_slots); + // Create the "bridge_out" component + let let_storage_slot_name = StorageSlotName::new("miden::agglayer::let").unwrap(); + let bridge_out_storage_slots = vec![StorageSlot::with_empty_map(let_storage_slot_name)]; + let bridge_out_component = bridge_out_component(bridge_out_storage_slots); + + // Combine the components into a single account(builder) Account::builder(seed.into()) - .storage_mode(AccountStorageMode::Public) - .with_component(bridge_in_comp) - .with_component(bridge_out_comp) + .storage_mode(AccountStorageMode::Network) + .with_component(bridge_out_component) + .with_component(bridge_in_component) } /// Creates a new bridge account with the standard configuration. diff --git a/crates/miden-agglayer/src/update_ger_note.rs b/crates/miden-agglayer/src/update_ger_note.rs index d0f0b608b3..f760f0f604 100644 --- a/crates/miden-agglayer/src/update_ger_note.rs +++ b/crates/miden-agglayer/src/update_ger_note.rs @@ -1,38 +1,115 @@ +//! UPDATE_GER note creation utilities. +//! +//! This module provides helpers for creating UPDATE_GER notes, +//! which are used to update the Global Exit Root in the bridge account. + extern crate alloc; +use alloc::string::ToString; use alloc::vec; +use miden_assembly::utils::Deserializable; +use miden_core::{Program, Word}; +use miden_protocol::account::AccountId; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::errors::NoteError; -use miden_protocol::note::{Note, NoteAssets, NoteMetadata, NoteRecipient, NoteStorage, NoteType}; +use miden_protocol::note::{ + Note, + NoteAssets, + NoteAttachment, + NoteExecutionHint, + NoteMetadata, + NoteRecipient, + NoteScript, + NoteStorage, + NoteType, +}; +use miden_standards::note::NetworkAccountTarget; +use miden_utils_sync::LazyLock; + +use crate::ExitRoot; + +// NOTE SCRIPT +// ================================================================================================ -use crate::{ExitRoot, update_ger_script}; +// Initialize the UPDATE_GER note script only once +static UPDATE_GER_SCRIPT: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/UPDATE_GER.masb")); + let program = + Program::read_from_bytes(bytes).expect("Shipped UPDATE_GER script is well-formed"); + NoteScript::new(program) +}); -/// Creates an UPDATE_GER note with the given GER (Global Exit Root) data. +// UPDATE_GER NOTE +// ================================================================================================ + +/// UPDATE_GER note. /// -/// The note storage contains 8 felts: GER[0..7] -pub fn create_update_ger_note( - ger: ExitRoot, - sender_account_id: miden_protocol::account::AccountId, - rng: &mut R, -) -> Result { - let update_ger_script = update_ger_script(); +/// This note is used to update the Global Exit Root (GER) in the bridge account. +/// It carries the new GER data and is always public. +pub struct UpdateGerNote; + +impl UpdateGerNote { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// Expected number of storage items for an UPDATE_GER note. + pub const NUM_STORAGE_ITEMS: usize = 8; + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the UPDATE_GER note script. + pub fn script() -> NoteScript { + UPDATE_GER_SCRIPT.clone() + } + + /// Returns the UPDATE_GER note script root. + pub fn script_root() -> Word { + UPDATE_GER_SCRIPT.root() + } + + // BUILDERS + // -------------------------------------------------------------------------------------------- - // Create note storage with 8 felts: GER[0..7] - let storage_values = ger.to_elements().to_vec(); + /// Creates an UPDATE_GER note with the given GER (Global Exit Root) data. + /// + /// The note storage contains 8 felts: GER[0..7] + /// + /// # Parameters + /// - `ger`: The Global Exit Root data + /// - `sender_account_id`: The account ID of the note creator + /// - `target_account_id`: The account ID that will consume this note (bridge account) + /// - `rng`: Random number generator for creating the note serial number + /// + /// # Errors + /// Returns an error if note creation fails. + pub fn create( + ger: ExitRoot, + sender_account_id: AccountId, + target_account_id: AccountId, + rng: &mut R, + ) -> Result { + // Create note storage with 8 felts: GER[0..7] + let storage_values = ger.to_elements().to_vec(); - let note_storage = NoteStorage::new(storage_values)?; + let note_storage = NoteStorage::new(storage_values)?; - // Generate a serial number for the note - let serial_num = rng.draw_word(); + // Generate a serial number for the note + let serial_num = rng.draw_word(); - let recipient = NoteRecipient::new(serial_num, update_ger_script, note_storage); + let recipient = NoteRecipient::new(serial_num, Self::script(), note_storage); - // Create note metadata - use a simple public tag - let metadata = NoteMetadata::new(sender_account_id, NoteType::Public); + let attachment = NoteAttachment::from( + NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always) + .map_err(|e| NoteError::other(e.to_string()))?, + ); + let metadata = + NoteMetadata::new(sender_account_id, NoteType::Public).with_attachment(attachment); - // UPDATE_GER notes don't carry assets - let assets = NoteAssets::new(vec![])?; + // UPDATE_GER notes don't carry assets + let assets = NoteAssets::new(vec![])?; - Ok(Note::new(assets, metadata, recipient)) + Ok(Note::new(assets, metadata, recipient)) + } } diff --git a/crates/miden-standards/asm/standards/attachments/network_account_target.masm b/crates/miden-standards/asm/standards/attachments/network_account_target.masm index 9c097162bc..46133f4136 100644 --- a/crates/miden-standards/asm/standards/attachments/network_account_target.masm +++ b/crates/miden-standards/asm/standards/attachments/network_account_target.masm @@ -2,6 +2,8 @@ #! #! Provides a standardized way to work with network account targets. +use miden::protocol::account_id +use miden::protocol::active_account use miden::protocol::active_note use miden::protocol::note @@ -18,32 +20,45 @@ pub const NETWORK_ACCOUNT_TARGET_ATTACHMENT_KIND = 1 # ERRORS # ================================================================================================ -const ERR_ATTACHMENT_SCHEME_MISMATCH = "expected network account target attachment scheme" -const ERR_ATTACHMENT_KIND_MISMATCH = "expected attachment kind to be Word for network account target" +const ERR_NOT_NETWORK_ACCOUNT_TARGET = "attachment is not a valid network account target" + +#! Returns a boolean indicating whether the attachment scheme and kind match the expected +#! values for a NetworkAccountTarget attachment. +#! +#! Inputs: [attachment_scheme, attachment_kind] +#! Outputs: [is_network_account_target] +#! +#! Invocation: exec +pub proc is_network_account_target + eq.NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME + # => [is_scheme_valid, attachment_kind] + + swap eq.NETWORK_ACCOUNT_TARGET_ATTACHMENT_KIND + # => [is_kind_valid, is_scheme_valid] + + and + # => [is_network_account_target] +end #! Returns the account ID encoded in the attachment. #! #! The attachment is expected to have the following layout: #! [0, exec_hint_tag, account_id_prefix, account_id_suffix] #! +#! WARNING: This procedure does not validate the attachment scheme or kind. The caller +#! should validate these using `is_network_account_target` before calling this procedure. +#! #! WARNING: This procedure does not validate that the returned account ID is well-formed. #! The caller should validate the account ID if needed using `account_id::validate`. #! -#! Inputs: [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] +#! Inputs: [NOTE_ATTACHMENT] #! Outputs: [account_id_prefix, account_id_suffix] #! #! Where: #! - account_id_{prefix,suffix} are the prefix and suffix felts of an account ID. #! -#! Panics if: -#! - the attachment scheme does not match NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME. -#! #! Invocation: exec pub proc get_id - # verify that the attachment scheme and kind are correct - # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] - eq.NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME assert.err=ERR_ATTACHMENT_SCHEME_MISMATCH - eq.NETWORK_ACCOUNT_TARGET_ATTACHMENT_KIND assert.err=ERR_ATTACHMENT_KIND_MISMATCH # => [NOTE_ATTACHMENT] = [0, exec_hint_tag, account_id_prefix, account_id_suffix] drop drop @@ -70,3 +85,45 @@ pub proc new push.NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME # => [attachment_scheme, attachment_kind, ATTACHMENT] end + +#! Returns a boolean indicating whether the active account matches the target account +#! encoded in the active note's attachment. +#! +#! Inputs: [] +#! Outputs: [is_equal] +#! +#! Where: +#! - is_equal is a boolean indicating whether the active account matches the target account. +#! +#! Panics if: +#! - the attachment is not a valid network account target. +#! +#! Invocation: exec +pub proc active_account_matches_target_account + # ensure note attachment targets the consuming bridge account + exec.active_note::get_metadata + # => [NOTE_ATTACHMENT, METADATA_HEADER] + + swapw + # => [METADATA_HEADER, NOTE_ATTACHMENT] + + exec.note::extract_attachment_info_from_metadata + # => [attachment_kind, attachment_scheme, NOTE_ATTACHMENT] + + swap + # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] + + # ensure the attachment is a network account target + exec.is_network_account_target assert.err=ERR_NOT_NETWORK_ACCOUNT_TARGET + + # => [NOTE_ATTACHMENT] = [0, exec_hint_tag, account_id_prefix, account_id_suffix] + + exec.get_id + # => [account_id_prefix, account_id_suffix] + + exec.active_account::get_id + # => [account_id_prefix, account_id_suffix, target_id_prefix, target_id_suffix] + + exec.account_id::is_equal + # => [is_equal] +end diff --git a/crates/miden-standards/src/errors/standards.rs b/crates/miden-standards/src/errors/standards.rs index 261f80e062..f16cc23ed4 100644 --- a/crates/miden-standards/src/errors/standards.rs +++ b/crates/miden-standards/src/errors/standards.rs @@ -9,11 +9,6 @@ use miden_protocol::errors::MasmError; // STANDARDS ERRORS // ================================================================================================ -/// Error Message: "expected attachment kind to be Word for network account target" -pub const ERR_ATTACHMENT_KIND_MISMATCH: MasmError = MasmError::from_static_str("expected attachment kind to be Word for network account target"); -/// Error Message: "expected network account target attachment scheme" -pub const ERR_ATTACHMENT_SCHEME_MISMATCH: MasmError = MasmError::from_static_str("expected network account target attachment scheme"); - /// Error Message: "burn requires exactly 1 note asset" pub const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("burn requires exactly 1 note asset"); @@ -36,6 +31,9 @@ pub const ERR_MINT_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::fr /// Error Message: "note tag length can be at most 32" pub const ERR_NOTE_TAG_MAX_ACCOUNT_TARGET_LENGTH_EXCEEDED: MasmError = MasmError::from_static_str("note tag length can be at most 32"); +/// Error Message: "attachment is not a valid network account target" +pub const ERR_NOT_NETWORK_ACCOUNT_TARGET: MasmError = MasmError::from_static_str("attachment is not a valid network account target"); + /// Error Message: "failed to reclaim P2IDE note because the reclaiming account is not the sender" pub const ERR_P2IDE_RECLAIM_ACCT_IS_NOT_SENDER: MasmError = MasmError::from_static_str("failed to reclaim P2IDE note because the reclaiming account is not the sender"); /// Error Message: "P2IDE reclaim is disabled" diff --git a/crates/miden-testing/src/standards/network_account_target.rs b/crates/miden-testing/src/standards/network_account_target.rs index e3d0b0f798..bd07dfe44a 100644 --- a/crates/miden-testing/src/standards/network_account_target.rs +++ b/crates/miden-testing/src/standards/network_account_target.rs @@ -26,12 +26,21 @@ async fn network_account_target_get_id() -> anyhow::Result<()> { use miden::standards::attachments::network_account_target use miden::protocol::note + const ERR_NOT_NETWORK_ACCOUNT_TARGET = "attachment is not a valid network account target" + begin push.{attachment_word} push.{metadata_header} exec.note::extract_attachment_info_from_metadata # => [attachment_kind, attachment_scheme, NOTE_ATTACHMENT] + swap + # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] + exec.network_account_target::is_network_account_target + # => [is_valid, NOTE_ATTACHMENT] + assert.err=ERR_NOT_NETWORK_ACCOUNT_TARGET + # => [NOTE_ATTACHMENT] exec.network_account_target::get_id + # => [account_id_prefix, account_id_suffix] # cleanup stack movup.2 drop movup.2 drop end @@ -104,6 +113,8 @@ async fn network_account_target_attachment_round_trip() -> anyhow::Result<()> { r#" use miden::standards::attachments::network_account_target + const ERR_NOT_NETWORK_ACCOUNT_TARGET = "attachment is not a valid network account target" + begin push.{exec_hint} push.{target_id_suffix} @@ -111,8 +122,13 @@ async fn network_account_target_attachment_round_trip() -> anyhow::Result<()> { # => [target_id_prefix, target_id_suffix, exec_hint] exec.network_account_target::new # => [attachment_scheme, attachment_kind, ATTACHMENT] + exec.network_account_target::is_network_account_target + # => [is_valid, ATTACHMENT] + assert.err=ERR_NOT_NETWORK_ACCOUNT_TARGET + # => [ATTACHMENT] exec.network_account_target::get_id # => [target_id_prefix, target_id_suffix] + # cleanup stack movup.2 drop movup.2 drop end "#, diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 72e8ec48b1..2ba7682dc2 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -1,32 +1,16 @@ extern crate alloc; -use miden_agglayer::{EthAddressFormat, b2agg_script, bridge_out_component}; -use miden_protocol::account::{ - Account, - AccountId, - AccountIdVersion, - AccountStorageMode, - AccountType, - StorageSlot, - StorageSlotName, -}; +use miden_agglayer::errors::ERR_B2AGG_TARGET_ACCOUNT_MISMATCH; +use miden_agglayer::{B2AggNote, EthAddressFormat, create_existing_bridge_account}; +use miden_crypto::rand::FeltRng; +use miden_protocol::Felt; +use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; use miden_protocol::asset::{Asset, FungibleAsset}; -use miden_protocol::note::{ - Note, - NoteAssets, - NoteMetadata, - NoteRecipient, - NoteScript, - NoteStorage, - NoteTag, - NoteType, -}; +use miden_protocol::note::{NoteAssets, NoteScript, NoteTag, NoteType}; use miden_protocol::transaction::OutputNote; -use miden_protocol::{Felt, Word}; use miden_standards::account::faucets::NetworkFungibleFaucet; use miden_standards::note::StandardNote; -use miden_testing::{AccountState, Auth, MockChain}; -use rand::Rng; +use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; /// Tests the B2AGG (Bridge to AggLayer) note script with bridge_out account component. /// @@ -52,50 +36,33 @@ async fn test_bridge_out_consumes_b2agg_note() -> anyhow::Result<()> { let faucet = builder.add_existing_network_faucet("AGG", 1000, faucet_owner_account_id, Some(100))?; - // Create a bridge account with the bridge_out component using network (public) storage - // Add a storage map for the bridge component to store MMR frontier data - let storage_slot_name = StorageSlotName::new("miden::agglayer::let").unwrap(); - let storage_slots = vec![StorageSlot::with_empty_map(storage_slot_name)]; - let bridge_component = bridge_out_component(storage_slots); - let account_builder = Account::builder(builder.rng_mut().random()) - .storage_mode(AccountStorageMode::Public) - .with_component(bridge_component); - let mut bridge_account = - builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)?; + // Create a bridge account (includes a `bridge_out` component tested here) + let mut bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + builder.add_account(bridge_account.clone())?; // CREATE B2AGG NOTE WITH ASSETS // -------------------------------------------------------------------------------------------- let amount = Felt::new(100); let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); - let tag = NoteTag::new(0); - let note_type = NoteType::Public; // Use Public note type for network transaction - - // Get the B2AGG note script - let b2agg_script = b2agg_script(); // Create note storage with destination network and address - // destination_network: u32 (AggLayer-assigned network ID) - // destination_address: 20 bytes (Ethereum address) split into 5 u32 values - let destination_network = Felt::new(1); // Example network ID + let destination_network = 1u32; // Example network ID let destination_address = "0x1234567890abcdef1122334455667788990011aa"; let eth_address = EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); - let address_felts = eth_address.to_elements().to_vec(); - - // Combine network ID and address felts into note storage (6 felts total) - let mut input_felts = vec![destination_network]; - input_felts.extend(address_felts); - let inputs = NoteStorage::new(input_felts.clone())?; + let assets = NoteAssets::new(vec![bridge_asset])?; - // Create the B2AGG note with assets from the faucet - let b2agg_note_metadata = NoteMetadata::new(faucet.id(), note_type).with_tag(tag); - let b2agg_note_assets = NoteAssets::new(vec![bridge_asset])?; - let serial_num = Word::from([1, 2, 3, 4u32]); - let b2agg_note_script = NoteScript::new(b2agg_script); - let b2agg_note_recipient = NoteRecipient::new(serial_num, b2agg_note_script, inputs); - let b2agg_note = Note::new(b2agg_note_assets, b2agg_note_metadata, b2agg_note_recipient); + // Create the B2AGG note using the helper + let b2agg_note = B2AggNote::create( + destination_network, + eth_address, + assets, + bridge_account.id(), + faucet.id(), + builder.rng_mut(), + )?; // Add the B2AGG note to the mock chain builder.add_output_note(OutputNote::Full(b2agg_note.clone())); @@ -219,6 +186,10 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let faucet = builder.add_existing_network_faucet("AGG", 1000, faucet_owner_account_id, Some(100))?; + // Create a bridge account (includes a `bridge_out` component tested here) + let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + builder.add_account(bridge_account.clone())?; + // Create a user account that will create and consume the B2AGG note let mut user_account = builder.add_existing_wallet(Auth::BasicAuth)?; @@ -227,33 +198,25 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let amount = Felt::new(50); let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); - let tag = NoteTag::new(0); - let note_type = NoteType::Public; - - // Get the B2AGG note script - let b2agg_script = b2agg_script(); // Create note storage with destination network and address - let destination_network = Felt::new(1); + let destination_network = 1u32; let destination_address = "0x1234567890abcdef1122334455667788990011aa"; let eth_address = EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); - let address_felts = eth_address.to_elements().to_vec(); - - // Combine network ID and address felts into note storage (6 felts total) - let mut input_felts = vec![destination_network]; - input_felts.extend(address_felts); - let inputs = NoteStorage::new(input_felts.clone())?; + let assets = NoteAssets::new(vec![bridge_asset])?; // Create the B2AGG note with the USER ACCOUNT as the sender // This is the key difference - the note sender will be the same as the consuming account - let b2agg_note_metadata = NoteMetadata::new(user_account.id(), note_type).with_tag(tag); - let b2agg_note_assets = NoteAssets::new(vec![bridge_asset])?; - let serial_num = Word::from([1, 2, 3, 4u32]); - let b2agg_note_script = NoteScript::new(b2agg_script); - let b2agg_note_recipient = NoteRecipient::new(serial_num, b2agg_note_script, inputs); - let b2agg_note = Note::new(b2agg_note_assets, b2agg_note_metadata, b2agg_note_recipient); + let b2agg_note = B2AggNote::create( + destination_network, + eth_address, + assets, + bridge_account.id(), + user_account.id(), + builder.rng_mut(), + )?; // Add the B2AGG note to the mock chain builder.add_output_note(OutputNote::Full(b2agg_note.clone())); @@ -297,3 +260,84 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { Ok(()) } + +/// Tests that a non-target account cannot consume a B2AGG note (non-reclaim branch). +/// +/// This test covers the security check in the B2AGG note script that ensures only the +/// designated target account (specified in the note attachment) can consume the note +/// when not in reclaim mode. +/// +/// Test flow: +/// 1. Creates a network faucet to provide assets +/// 2. Creates a bridge account as the designated target for the B2AGG note +/// 3. Creates a user account as the sender (creator) of the B2AGG note +/// 4. Creates a "malicious" account with a bridge interface +/// 5. Attempts to consume the B2AGG note with the malicious account +/// 6. Verifies that the transaction fails with ERR_B2AGG_TARGET_ACCOUNT_MISMATCH +#[tokio::test] +async fn test_b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + // Create a network faucet owner account + let faucet_owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + // Create a network faucet to provide assets for the B2AGG note + let faucet = + builder.add_existing_network_faucet("AGG", 1000, faucet_owner_account_id, Some(100))?; + + // Create a bridge account as the designated TARGET for the B2AGG note + let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + builder.add_account(bridge_account.clone())?; + + // Create a user account as the SENDER of the B2AGG note + let sender_account = builder.add_existing_wallet(Auth::BasicAuth)?; + + // Create a "malicious" account with a bridge interface + let malicious_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + builder.add_account(malicious_account.clone())?; + + // CREATE B2AGG NOTE + // -------------------------------------------------------------------------------------------- + + let amount = Felt::new(50); + let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); + + // Create note storage with destination network and address + let destination_network = 1u32; + let destination_address = "0x1234567890abcdef1122334455667788990011aa"; + let eth_address = + EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); + + let assets = NoteAssets::new(vec![bridge_asset])?; + + // Create the B2AGG note + let b2agg_note = B2AggNote::create( + destination_network, + eth_address, + assets, + bridge_account.id(), + sender_account.id(), + builder.rng_mut(), + )?; + + // Add the B2AGG note to the mock chain + builder.add_output_note(OutputNote::Full(b2agg_note.clone())); + let mock_chain = builder.build()?; + + // ATTEMPT TO CONSUME B2AGG NOTE WITH MALICIOUS ACCOUNT (SHOULD FAIL) + // -------------------------------------------------------------------------------------------- + let result = mock_chain + .build_tx_context(malicious_account.id(), &[], &[b2agg_note])? + .build()? + .execute() + .await; + + assert_transaction_executor_error!(result, ERR_B2AGG_TARGET_ACCOUNT_MISMATCH); + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/update_ger.rs b/crates/miden-testing/tests/agglayer/update_ger.rs index 06c35ad6ee..6b2f973ae9 100644 --- a/crates/miden-testing/tests/agglayer/update_ger.rs +++ b/crates/miden-testing/tests/agglayer/update_ger.rs @@ -1,9 +1,9 @@ -use miden_agglayer::{ExitRoot, create_existing_bridge_account, create_update_ger_note}; +use miden_agglayer::{ExitRoot, UpdateGerNote, create_existing_bridge_account}; use miden_protocol::Word; use miden_protocol::account::StorageSlotName; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::transaction::OutputNote; -use miden_testing::MockChain; +use miden_testing::{Auth, MockChain}; #[tokio::test] async fn test_update_ger_note_updates_storage() -> anyhow::Result<()> { @@ -15,7 +15,12 @@ async fn test_update_ger_note_updates_storage() -> anyhow::Result<()> { let bridge_account = create_existing_bridge_account(bridge_seed); builder.add_account(bridge_account.clone())?; - // CREATE UPDATE_GER NOTE WITH 8 STORAGE ITEMS + // CREATE USER ACCOUNT (NOTE SENDER) + // -------------------------------------------------------------------------------------------- + let user_account = builder.add_existing_wallet(Auth::BasicAuth)?; + builder.add_account(user_account.clone())?; + + // CREATE UPDATE_GER NOTE WITH 8 STORAGE ITEMS (NEW GER AS TWO WORDS) // -------------------------------------------------------------------------------------------- let ger_bytes: [u8; 32] = [ @@ -24,7 +29,8 @@ async fn test_update_ger_note_updates_storage() -> anyhow::Result<()> { 0x77, 0x88, ]; let ger = ExitRoot::from(ger_bytes); - let update_ger_note = create_update_ger_note(ger, bridge_account.id(), builder.rng_mut())?; + let update_ger_note = + UpdateGerNote::create(ger, user_account.id(), bridge_account.id(), builder.rng_mut())?; builder.add_output_note(OutputNote::Full(update_ger_note.clone())); let mock_chain = builder.build()?;