From fc29b7c8c73116bdc3af1a496630e6a0650cea7d Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 18 May 2026 10:45:19 -0300 Subject: [PATCH 01/11] rework SWAP to support public payback notes Public payback notes were unrecoverable from on-chain data: the previous SWAP precomputed the payback recipient off-line and embedded only the resulting hash, so the consuming script had no preimage to register with the advice provider. Build the payback P2ID recipient at consume time from data available in SWAP storage so the on-chain script can call p2id::new (which also registers the recipient preimage in the advice map): - Embed the creator account ID in storage (hybrid embed, mirroring PSWAP) so the consumer reads it directly instead of going through active_note::get_sender. - Derive the payback serial as swap_serial[0] + 1. - Derive the payback tag from the creator account ID prefix via note_tag::create_account_target. Storage shrinks from 14 to 11 items: the precomputed recipient and tag are no longer stored. The Rust constructor sets creator_id = sender by convention. --- .../asm/standards/notes/swap.masm | 99 +++++++--- crates/miden-standards/src/note/mod.rs | 2 +- crates/miden-standards/src/note/swap.rs | 186 ++++++++++-------- crates/miden-testing/tests/scripts/swap.rs | 52 +++++ docs/src/note.md | 9 +- 5 files changed, 230 insertions(+), 118 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/swap.masm b/crates/miden-standards/asm/standards/notes/swap.masm index cfd11c1138..42b4ff5e83 100644 --- a/crates/miden-standards/asm/standards/notes/swap.masm +++ b/crates/miden-standards/asm/standards/notes/swap.masm @@ -1,28 +1,50 @@ use miden::protocol::active_note use miden::protocol::asset -use miden::protocol::output_note +use miden::standards::note_tag +use miden::standards::notes::p2id use miden::standards::wallets::basic->wallet # CONSTANTS # ================================================================================================= -const SWAP_NOTE_NUM_STORAGE_ITEMS=14 - +const SWAP_NOTE_NUM_STORAGE_ITEMS=11 + +# Note storage layout (11 felts, loaded at STORAGE_PTR by get_storage): +# - requested_asset_key [0..3] : 4 felts +# - requested_asset_value [4..7] : 4 felts +# - payback_note_type [8] : 1 felt +# - creator_id_prefix [9] : 1 felt +# - creator_id_suffix [10] : 1 felt +# +# The payback note is a P2ID note paying the requested asset back to the SWAP's creator. The +# creator account ID is embedded explicitly in storage (slots 9-10). +# The Rust constructor sets `creator_id = sender`; the MASM trusts the +# embedded value and does not cross-check against the active note's metadata sender. +# +# The payback recipient is derived deterministically: +# - script: well-known P2ID script root (resolved by `p2id::new` via `procref` at runtime) +# - inputs: creator account id (loaded from storage slots 9-10) +# - serial: SWAP's own serial with the least significant element incremented by 1 +# - tag: derived from creator account id prefix via `note_tag::create_account_target` +const STORAGE_PTR=0 const REQUESTED_ASSET_PTR=0 -const PAYBACK_RECIPIENT_PTR=8 -const PAYBACK_NOTE_TYPE_PTR=12 -const PAYBACK_NOTE_TAG_PTR=13 +const PAYBACK_NOTE_TYPE_PTR=8 +const CREATOR_PREFIX_PTR=9 +const CREATOR_SUFFIX_PTR=10 + +# `write_storage_to_memory` pads the storage write up to an even number of words (16 felts for 11 +# storage items), so memory[11..15] is padding after `get_storage`. Place the asset region past it. const ASSET_PTR=16 -# ERRORS +# ERRORS # ================================================================================================= -const ERR_SWAP_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS="SWAP script expects exactly 14 note storage items" +const ERR_SWAP_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS="SWAP script expects exactly 11 note storage items" const ERR_SWAP_WRONG_NUMBER_OF_ASSETS="SWAP script requires exactly 1 note asset" -#! Swap script: adds an asset from the note into consumers account and -#! creates a note consumable by note issuer containing requested asset. +#! Swap script: adds an asset from the note into the consumer's account and creates a P2ID payback +#! note addressed to the creator carrying the requested asset. #! #! Requires that the account exposes: #! - miden::standards::wallets::basic::receive_asset procedure. @@ -31,13 +53,6 @@ const ERR_SWAP_WRONG_NUMBER_OF_ASSETS="SWAP script requires exactly 1 note asset #! Inputs: [ARGS] #! Outputs: [] #! -#! Note storage is assumed to be as follows: -#! - REQUESTED_ASSET_KEY -#! - REQUESTED_ASSET_VALUE -#! - PAYBACK_RECIPIENT -#! - payback_note_type -#! - payback_note_tag -#! #! Panics if: #! - account does not expose miden::standards::wallets::basic::receive_asset procedure. #! - account does not expose miden::standards::wallets::basic::move_asset_to_note procedure. @@ -46,32 +61,54 @@ const ERR_SWAP_WRONG_NUMBER_OF_ASSETS="SWAP script requires exactly 1 note asset #! greater than 2^63. @note_script pub proc main - # dropping note args + # drop note args dropw # => [] - # --- create a payback note with the requested asset ---------------- - - # store note storage into memory starting at address 0 - push.0 exec.active_note::get_storage + # store note storage into memory starting at STORAGE_PTR + push.STORAGE_PTR exec.active_note::get_storage # => [num_storage_items] # check number of storage items eq.SWAP_NOTE_NUM_STORAGE_ITEMS assert.err=ERR_SWAP_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS # => [] - padw mem_loadw_le.PAYBACK_RECIPIENT_PTR - # => [PAYBACK_NOTE_RECIPIENT] + # --- create payback P2ID note - # load payback P2ID details - mem_load.PAYBACK_NOTE_TYPE_PTR - mem_load.PAYBACK_NOTE_TAG_PTR - # => [tag, note_type, PAYBACK_NOTE_RECIPIENT] + # Derive P2ID serial = SWAP_SERIAL with the least significant element +1. + # The creator can recompute this offline from SWAP's own serial number. + exec.active_note::get_serial_number + # => [s0, s1, s2, s3] + + add.1 + # => [s0', s1, s2, s3] - # create payback P2ID note - exec.output_note::create + # Push payback note type above the serial. + mem_load.PAYBACK_NOTE_TYPE_PTR + # => [note_type, s0', s1, s2, s3] + + # Load the creator id (= SWAP sender, by constructor convention) from storage. Loading prefix + # first then suffix leaves suffix on top, matching `active_note::get_sender`'s stack shape. + mem_load.CREATOR_PREFIX_PTR + mem_load.CREATOR_SUFFIX_PTR + # => [creator_suffix, creator_prefix, note_type, s0', s1, s2, s3] + + # Derive payback tag from creator prefix. + dup.1 + # => [creator_prefix, creator_suffix, creator_prefix, note_type, s0', s1, s2, s3] + exec.note_tag::create_account_target + # => [tag, creator_suffix, creator_prefix, note_type, s0', s1, s2, s3] + + # Reshape to match `p2id::new`'s signature: + # [target_id_suffix, target_id_prefix, tag, note_type, SERIAL_NUM]. + movdn.2 + # => [creator_suffix, creator_prefix, tag, note_type, s0', s1, s2, s3] + + exec.p2id::new # => [note_idx] + # --- move requested asset into the payback P2ID note + padw push.0.0.0 movup.7 # => [note_idx, pad(7)] @@ -85,7 +122,7 @@ pub proc main dropw dropw # => [pad(8)] - # --- move assets from the SWAP note into the account ------------------------- + # --- move offered asset from the SWAP note into the consumer's account # store the number of note assets to memory starting at address ASSET_PTR push.ASSET_PTR exec.active_note::get_assets diff --git a/crates/miden-standards/src/note/mod.rs b/crates/miden-standards/src/note/mod.rs index f89fa10d3e..8500a2103d 100644 --- a/crates/miden-standards/src/note/mod.rs +++ b/crates/miden-standards/src/note/mod.rs @@ -29,7 +29,7 @@ mod pswap; pub use pswap::{PswapNote, PswapNoteStorage}; mod swap; -pub use swap::{SwapNote, SwapNoteStorage}; +pub use swap::{SwapNote, SwapNoteStorage, payback_serial_from_swap}; mod network_account_target; pub use network_account_target::{NetworkAccountTarget, NetworkAccountTargetError}; diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index 53ea5ba57b..8adcab56d4 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -1,6 +1,5 @@ use alloc::vec::Vec; -use miden_protocol::Word; use miden_protocol::account::AccountId; use miden_protocol::assembly::Path; use miden_protocol::asset::Asset; @@ -20,6 +19,7 @@ use miden_protocol::note::{ NoteType, }; use miden_protocol::utils::sync::LazyLock; +use miden_protocol::{Felt, ONE, Word}; use crate::StandardsLib; use crate::note::P2idNoteStorage; @@ -89,10 +89,7 @@ impl SwapNote { return Err(NoteError::other("requested asset same as offered asset")); } - let payback_serial_num = rng.draw_word(); - - let swap_storage = - SwapNoteStorage::new(sender, requested_asset, payback_note_type, payback_serial_num); + let swap_storage = SwapNoteStorage::new(requested_asset, payback_note_type, sender); let serial_num = rng.draw_word(); let recipient = swap_storage.into_recipient(serial_num); @@ -108,6 +105,7 @@ impl SwapNote { let note = Note::new(assets, metadata, recipient); // build the payback note details + let payback_serial_num = payback_serial_from_swap(serial_num); let payback_recipient = P2idNoteStorage::new(sender).into_recipient(payback_serial_num); let payback_assets = NoteAssets::new(vec![requested_asset])?; let payback_note = NoteDetails::new(payback_assets, payback_recipient); @@ -160,15 +158,21 @@ impl SwapNote { /// Canonical storage representation for a SWAP note. /// -/// Contains the payback note configuration and the requested asset that the -/// swap creator wants to receive in exchange for the offered asset contained -/// in the note's vault. +/// Maps to the 11-element [`NoteStorage`] layout consumed by the on-chain MASM script: +/// +/// | Slot | Field | +/// |----------|-------| +/// | `[0-7]` | Requested asset (key + value) | +/// | `[8]` | Payback note type (0 = private, 1 = public) | +/// | `[9-10]` | Creator account ID (prefix, suffix) | +/// +/// The payback note tag is derived at runtime from the creator account ID +/// (via `note_tag::create_account_target` in MASM). #[derive(Debug, Clone, PartialEq, Eq)] pub struct SwapNoteStorage { payback_note_type: NoteType, - payback_tag: NoteTag, requested_asset: Asset, - payback_recipient_digest: Word, + creator_account_id: AccountId, } impl SwapNoteStorage { @@ -176,41 +180,21 @@ impl SwapNoteStorage { // -------------------------------------------------------------------------------------------- /// Expected number of storage items of the SWAP note. - pub const NUM_ITEMS: usize = 14; + pub const NUM_ITEMS: usize = 11; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- /// Creates new SWAP note storage with the specified parameters. pub fn new( - sender: AccountId, requested_asset: Asset, payback_note_type: NoteType, - payback_serial_number: Word, - ) -> Self { - let payback_recipient = P2idNoteStorage::new(sender).into_recipient(payback_serial_number); - let payback_tag = NoteTag::with_account_target(sender); - - Self::from_parts( - payback_note_type, - payback_tag, - requested_asset, - payback_recipient.digest(), - ) - } - - /// Creates a [`SwapNoteStorage`] from raw parts. - pub fn from_parts( - payback_note_type: NoteType, - payback_tag: NoteTag, - requested_asset: Asset, - payback_recipient_digest: Word, + creator_account_id: AccountId, ) -> Self { Self { payback_note_type, - payback_tag, requested_asset, - payback_recipient_digest, + creator_account_id, } } @@ -219,19 +203,14 @@ impl SwapNoteStorage { self.payback_note_type } - /// Returns the payback note tag. - pub fn payback_tag(&self) -> NoteTag { - self.payback_tag - } - /// Returns the requested asset. pub fn requested_asset(&self) -> Asset { self.requested_asset } - /// Returns the payback recipient digest. - pub fn payback_recipient_digest(&self) -> Word { - self.payback_recipient_digest + /// Returns the creator account ID embedded in the SWAP storage. + pub fn creator_account_id(&self) -> AccountId { + self.creator_account_id } /// Consumes the storage and returns a SWAP [`NoteRecipient`] with the provided serial number. @@ -247,24 +226,70 @@ impl From for NoteStorage { fn from(storage: SwapNoteStorage) -> Self { let mut storage_values = Vec::with_capacity(SwapNoteStorage::NUM_ITEMS); storage_values.extend_from_slice(&storage.requested_asset.as_elements()); - storage_values.extend_from_slice(storage.payback_recipient_digest.as_elements()); - storage_values - .extend_from_slice(&[storage.payback_note_type.into(), storage.payback_tag.into()]); + storage_values.push(Felt::from(storage.payback_note_type.as_u8())); + storage_values.push(storage.creator_account_id.prefix().as_felt()); + storage_values.push(storage.creator_account_id.suffix()); NoteStorage::new(storage_values) .expect("number of storage items should not exceed max storage items") } } +/// Deserializes [`SwapNoteStorage`] from a slice of exactly 11 [`Felt`]s. +impl TryFrom<&[Felt]> for SwapNoteStorage { + type Error = NoteError; + + fn try_from(note_storage: &[Felt]) -> Result { + if note_storage.len() != Self::NUM_ITEMS { + return Err(NoteError::InvalidNoteStorageLength { + expected: Self::NUM_ITEMS, + actual: note_storage.len(), + }); + } + + // [0..7] = requested asset (key + value) + let key = Word::new([note_storage[0], note_storage[1], note_storage[2], note_storage[3]]); + let value = Word::new([note_storage[4], note_storage[5], note_storage[6], note_storage[7]]); + let requested_asset = Asset::from_key_value_words(key, value) + .map_err(|e| NoteError::other_with_source("failed to parse requested asset", e))?; + + // [8] = payback_note_type + let payback_note_type = NoteType::try_from( + u8::try_from(note_storage[8].as_canonical_u64()) + .map_err(|_| NoteError::other("payback_note_type exceeds u8"))?, + ) + .map_err(|e| NoteError::other_with_source("failed to parse payback note type", e))?; + + // [9..10] = creator account ID (prefix, suffix) + let creator_account_id = AccountId::try_from_elements(note_storage[10], note_storage[9]) + .map_err(|e| NoteError::other_with_source("failed to parse creator account ID", e))?; + + Ok(Self { + payback_note_type, + requested_asset, + creator_account_id, + }) + } +} + +/// Returns the P2ID payback serial derived from a SWAP note's own serial number. +/// +/// The SWAP MASM script computes the payback's serial by incrementing the least significant +/// element of the SWAP serial. Creators can recompute this offline to track or consume the +/// payback note after the SWAP is filled. +pub fn payback_serial_from_swap(swap_serial: Word) -> Word { + let elements = swap_serial.as_elements(); + Word::new([elements[0] + ONE, elements[1], elements[2], elements[3]]) +} + // TESTS // ================================================================================================ #[cfg(test)] mod tests { - use miden_protocol::Felt; use miden_protocol::account::{AccountIdVersion, AccountStorageMode, AccountType}; use miden_protocol::asset::{FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; - use miden_protocol::note::{NoteStorage, NoteTag, NoteType}; + use miden_protocol::note::{NoteStorage, NoteType}; use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, @@ -290,51 +315,48 @@ mod tests { Asset::NonFungible(NonFungibleAsset::new(&details).unwrap()) } + fn dummy_creator_id() -> AccountId { + AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + ) + } + #[test] - fn swap_note_storage() { - let payback_note_type = NoteType::Private; - let payback_tag = NoteTag::new(0x12345678); - let requested_asset = fungible_asset(); - let payback_recipient_digest = - Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); - - let storage = SwapNoteStorage::from_parts( - payback_note_type, - payback_tag, - requested_asset, - payback_recipient_digest, - ); + fn swap_note_storage_round_trip_fungible() { + let creator = dummy_creator_id(); + let storage = SwapNoteStorage::new(fungible_asset(), NoteType::Private, creator); - assert_eq!(storage.payback_note_type(), payback_note_type); - assert_eq!(storage.payback_tag(), payback_tag); - assert_eq!(storage.requested_asset(), requested_asset); - assert_eq!(storage.payback_recipient_digest(), payback_recipient_digest); + let note_storage = NoteStorage::from(storage.clone()); + assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS); + assert_eq!(storage.payback_note_type(), NoteType::Private); + assert_eq!(storage.requested_asset(), fungible_asset()); + assert_eq!(storage.creator_account_id(), creator); + } - // Convert to NoteStorage - let note_storage = NoteStorage::from(storage); + #[test] + fn swap_note_storage_round_trip_non_fungible_public() { + let creator = dummy_creator_id(); + let storage = SwapNoteStorage::new(non_fungible_asset(), NoteType::Public, creator); + + let note_storage = NoteStorage::from(storage.clone()); assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS); + assert_eq!(storage.payback_note_type(), NoteType::Public); + assert_eq!(storage.requested_asset(), non_fungible_asset()); + assert_eq!(storage.creator_account_id(), creator); } #[test] - fn swap_note_storage_with_non_fungible_asset() { - let payback_note_type = NoteType::Public; - let payback_tag = NoteTag::new(0xaabbccdd); - let requested_asset = non_fungible_asset(); - let payback_recipient_digest = - Word::new([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]); - - let storage = SwapNoteStorage::from_parts( - payback_note_type, - payback_tag, - requested_asset, - payback_recipient_digest, - ); + fn swap_note_storage_try_from_round_trip() { + let original = SwapNoteStorage::new(fungible_asset(), NoteType::Public, dummy_creator_id()); + let note_storage = NoteStorage::from(original.clone()); - assert_eq!(storage.payback_note_type(), payback_note_type); - assert_eq!(storage.requested_asset(), requested_asset); + let parsed = + SwapNoteStorage::try_from(note_storage.items()).expect("round trip should succeed"); - let note_storage = NoteStorage::from(storage); - assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS); + assert_eq!(parsed, original); } #[test] diff --git a/crates/miden-testing/tests/scripts/swap.rs b/crates/miden-testing/tests/scripts/swap.rs index 0cd95695a9..bc57609248 100644 --- a/crates/miden-testing/tests/scripts/swap.rs +++ b/crates/miden-testing/tests/scripts/swap.rs @@ -158,6 +158,58 @@ async fn consume_swap_note_private_payback_note() -> anyhow::Result<()> { Ok(()) } +// Consumes a SWAP note with a public payback without any off-band advice. The executor materializes +// the payback recipient from the creator account ID embedded in SWAP storage and the SWAP's own +// serial number, then registers it with the advice provider via `p2id::new -> +// note::build_recipient`. +#[tokio::test] +async fn consume_swap_note_public_payback_note_no_advice() -> anyhow::Result<()> { + let payback_note_type = NoteType::Public; + let SwapTestSetup { + mock_chain, + mut sender_account, + mut target_account, + offered_asset, + requested_asset, + swap_note, + payback_note, + } = setup_swap_test(payback_note_type)?; + + let consume_swap_note_tx = mock_chain + .build_tx_context(target_account.id(), &[swap_note.id()], &[]) + .context("failed to build tx context")? + .build()? + .execute() + .await?; + + target_account.apply_delta(consume_swap_note_tx.account_delta())?; + + let output_payback_note = consume_swap_note_tx.output_notes().iter().next().unwrap().clone(); + assert_eq!(output_payback_note.id(), payback_note.id()); + assert_eq!(output_payback_note.assets().iter().next().unwrap(), &requested_asset); + + assert_eq!(target_account.vault().assets().count(), 1); + assert!(target_account.vault().assets().any(|asset| asset == offered_asset)); + + let full_payback_note = Note::new( + payback_note.assets().clone(), + output_payback_note.metadata().clone(), + payback_note.recipient().clone(), + ); + + let consume_payback_tx = mock_chain + .build_tx_context(sender_account.id(), &[], &[full_payback_note]) + .context("failed to build tx context")? + .build()? + .execute() + .await?; + + sender_account.apply_delta(consume_payback_tx.account_delta())?; + assert!(sender_account.vault().assets().any(|asset| asset == requested_asset)); + + Ok(()) +} + // Creates a swap note with a public payback note, then consumes it to complete the swap // The target account receives the offered asset and creates a public payback note for the sender #[tokio::test] diff --git a/docs/src/note.md b/docs/src/note.md index b806faac84..af3e197dc1 100644 --- a/docs/src/note.md +++ b/docs/src/note.md @@ -226,10 +226,11 @@ The SWAP note script implements atomic asset swapping functionality. **Key characteristics:** - **Purpose:** Atomic asset exchange between two parties -- **Storage:** Requires exactly 16 storage items specifying: - - Requested asset details - - Payback note recipient information - - Note creation parameters (type, tag, attachment) +- **Storage:** Requires exactly 11 storage items specifying: + - Requested asset + - Payback note type + - Creator account ID prefix + - Creator account ID suffix - **Assets:** Must contain exactly 1 asset to be swapped - **Mechanism:** 1. Creates a payback note containing the requested asset for the original note issuer From 9c57457aa1399ccb9a919dc63914c6a3e5155031 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 18 May 2026 11:55:20 -0300 Subject: [PATCH 02/11] simplify swap.masm comments --- .../asm/standards/notes/swap.masm | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/swap.masm b/crates/miden-standards/asm/standards/notes/swap.masm index 42b4ff5e83..d7f01678ad 100644 --- a/crates/miden-standards/asm/standards/notes/swap.masm +++ b/crates/miden-standards/asm/standards/notes/swap.masm @@ -10,22 +10,15 @@ use miden::standards::wallets::basic->wallet const SWAP_NOTE_NUM_STORAGE_ITEMS=11 # Note storage layout (11 felts, loaded at STORAGE_PTR by get_storage): -# - requested_asset_key [0..3] : 4 felts -# - requested_asset_value [4..7] : 4 felts -# - payback_note_type [8] : 1 felt -# - creator_id_prefix [9] : 1 felt -# - creator_id_suffix [10] : 1 felt +# - requested_asset_key [0..3] +# - requested_asset_value [4..7] +# - payback_note_type [8] +# - creator_id_prefix [9] +# - creator_id_suffix [10] # -# The payback note is a P2ID note paying the requested asset back to the SWAP's creator. The -# creator account ID is embedded explicitly in storage (slots 9-10). -# The Rust constructor sets `creator_id = sender`; the MASM trusts the -# embedded value and does not cross-check against the active note's metadata sender. -# -# The payback recipient is derived deterministically: -# - script: well-known P2ID script root (resolved by `p2id::new` via `procref` at runtime) -# - inputs: creator account id (loaded from storage slots 9-10) -# - serial: SWAP's own serial with the least significant element incremented by 1 -# - tag: derived from creator account id prefix via `note_tag::create_account_target` +# The payback P2ID recipient is derived at consume time from this storage. The MASM +# trusts the embedded creator id by constructor convention (sender == creator) and does +# not cross-check it against the active note's metadata sender. const STORAGE_PTR=0 const REQUESTED_ASSET_PTR=0 const PAYBACK_NOTE_TYPE_PTR=8 @@ -87,8 +80,8 @@ pub proc main mem_load.PAYBACK_NOTE_TYPE_PTR # => [note_type, s0', s1, s2, s3] - # Load the creator id (= SWAP sender, by constructor convention) from storage. Loading prefix - # first then suffix leaves suffix on top, matching `active_note::get_sender`'s stack shape. + # Load creator id (prefix then suffix) so the stack ends with suffix on top, matching + # `active_note::get_sender`'s output shape. mem_load.CREATOR_PREFIX_PTR mem_load.CREATOR_SUFFIX_PTR # => [creator_suffix, creator_prefix, note_type, s0', s1, s2, s3] From 0f819e076c0891a0fec376e576c652581c6c1e5e Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 18 May 2026 16:11:05 -0300 Subject: [PATCH 03/11] refactor(standards): hide private SWAP payback recipient Branch SWAP MASM on payback note type so private paybacks store an opaque precomputed recipient digest (and tag) instead of the creator id. Public paybacks keep the creator id in plaintext since the consumer needs it to reconstruct the recipient via p2id::new. The unified 16-felt storage asserts zero on the slots unused by each branch, making the privacy guarantee structural rather than convention-based. --- .../asm/standards/notes/swap.masm | 113 ++++--- crates/miden-standards/src/note/swap.rs | 307 +++++++++++++++--- 2 files changed, 320 insertions(+), 100 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/swap.masm b/crates/miden-standards/asm/standards/notes/swap.masm index d7f01678ad..0ba35c594a 100644 --- a/crates/miden-standards/asm/standards/notes/swap.masm +++ b/crates/miden-standards/asm/standards/notes/swap.masm @@ -1,43 +1,53 @@ use miden::protocol::active_note use miden::protocol::asset -use miden::standards::note_tag +use miden::protocol::output_note +use miden::protocol::note::NOTE_TYPE_PRIVATE use miden::standards::notes::p2id use miden::standards::wallets::basic->wallet # CONSTANTS # ================================================================================================= -const SWAP_NOTE_NUM_STORAGE_ITEMS=11 +const SWAP_NOTE_NUM_STORAGE_ITEMS=16 -# Note storage layout (11 felts, loaded at STORAGE_PTR by get_storage): +# Note storage layout (16 felts, loaded at STORAGE_PTR by get_storage): # - requested_asset_key [0..3] # - requested_asset_value [4..7] -# - payback_note_type [8] -# - creator_id_prefix [9] -# - creator_id_suffix [10] +# - payback_recipient [8..11] (private mode only; zero in public mode) +# - payback_note_type [12] +# - payback_tag [13] +# - creator_id_prefix [14] (public mode only; zero in private mode) +# - creator_id_suffix [15] (public mode only; zero in private mode) # -# The payback P2ID recipient is derived at consume time from this storage. The MASM -# trusts the embedded creator id by constructor convention (sender == creator) and does -# not cross-check it against the active note's metadata sender. +# In private mode, the recipient digest and tag are precomputed off-chain by the creator and +# embedded as opaque values; the consumer of the SWAP cannot learn who the payback targets from +# storage alone. In public mode, the recipient must be reconstructible by any consumer, so the +# creator id is embedded in plaintext and the MASM derives the recipient at consume time. The +# payback tag is stored explicitly in both modes; the creator is responsible for picking a tag +# that targets the payback receiver in public mode. const STORAGE_PTR=0 const REQUESTED_ASSET_PTR=0 -const PAYBACK_NOTE_TYPE_PTR=8 -const CREATOR_PREFIX_PTR=9 -const CREATOR_SUFFIX_PTR=10 +const PAYBACK_RECIPIENT_PTR=8 +const PAYBACK_NOTE_TYPE_PTR=12 +const PAYBACK_TAG_PTR=13 +const CREATOR_PREFIX_PTR=14 +const CREATOR_SUFFIX_PTR=15 -# `write_storage_to_memory` pads the storage write up to an even number of words (16 felts for 11 -# storage items), so memory[11..15] is padding after `get_storage`. Place the asset region past it. +# Storage is exactly 16 felts = 4 words, so the asset region starts right after. const ASSET_PTR=16 # ERRORS # ================================================================================================= -const ERR_SWAP_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS="SWAP script expects exactly 11 note storage items" +const ERR_SWAP_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS="SWAP script expects exactly 16 note storage items" const ERR_SWAP_WRONG_NUMBER_OF_ASSETS="SWAP script requires exactly 1 note asset" -#! Swap script: adds an asset from the note into the consumer's account and creates a P2ID payback -#! note addressed to the creator carrying the requested asset. +#! Swap script: adds the offered asset from the note into the consumer's account and creates a +#! P2ID payback note addressed to the creator carrying the requested asset. +#! +#! The payback note type (selected by the creator) determines how the payback recipient is +#! derived: private uses an opaque precomputed digest, public derives it from the creator id. #! #! Requires that the account exposes: #! - miden::standards::wallets::basic::receive_asset procedure. @@ -47,8 +57,7 @@ const ERR_SWAP_WRONG_NUMBER_OF_ASSETS="SWAP script requires exactly 1 note asset #! Outputs: [] #! #! Panics if: -#! - account does not expose miden::standards::wallets::basic::receive_asset procedure. -#! - account does not expose miden::standards::wallets::basic::move_asset_to_note procedure. +#! - account does not expose the required wallet procedures. #! - account vault does not contain the requested asset. #! - adding a fungible asset would result in amount overflow, i.e., the total amount would be #! greater than 2^63. @@ -68,36 +77,44 @@ pub proc main # --- create payback P2ID note - # Derive P2ID serial = SWAP_SERIAL with the least significant element +1. - # The creator can recompute this offline from SWAP's own serial number. - exec.active_note::get_serial_number - # => [s0, s1, s2, s3] - - add.1 - # => [s0', s1, s2, s3] - - # Push payback note type above the serial. + # Branch on payback note type. mem_load.PAYBACK_NOTE_TYPE_PTR - # => [note_type, s0', s1, s2, s3] - - # Load creator id (prefix then suffix) so the stack ends with suffix on top, matching - # `active_note::get_sender`'s output shape. - mem_load.CREATOR_PREFIX_PTR - mem_load.CREATOR_SUFFIX_PTR - # => [creator_suffix, creator_prefix, note_type, s0', s1, s2, s3] - - # Derive payback tag from creator prefix. - dup.1 - # => [creator_prefix, creator_suffix, creator_prefix, note_type, s0', s1, s2, s3] - exec.note_tag::create_account_target - # => [tag, creator_suffix, creator_prefix, note_type, s0', s1, s2, s3] - - # Reshape to match `p2id::new`'s signature: - # [target_id_suffix, target_id_prefix, tag, note_type, SERIAL_NUM]. - movdn.2 - # => [creator_suffix, creator_prefix, tag, note_type, s0', s1, s2, s3] - - exec.p2id::new + eq.NOTE_TYPE_PRIVATE + # => [is_private] + + if.true + # --- PRIVATE PAYBACK --- + # Load the precomputed payback recipient (4 felts, word-aligned). + padw mem_loadw_le.PAYBACK_RECIPIENT_PTR + # => [PAYBACK_RECIPIENT] + + mem_load.PAYBACK_NOTE_TYPE_PTR + mem_load.PAYBACK_TAG_PTR + # => [tag, note_type, PAYBACK_RECIPIENT] + + exec.output_note::create + # => [note_idx] + else + # --- PUBLIC PAYBACK --- + # Derive P2ID serial = SWAP_SERIAL with the least significant element +1. + # The creator can recompute this offline from SWAP's own serial number. + exec.active_note::get_serial_number + add.1 + # => [s0', s1, s2, s3] + + mem_load.PAYBACK_NOTE_TYPE_PTR + mem_load.PAYBACK_TAG_PTR + # => [tag, note_type, s0', s1, s2, s3] + + # Load creator id (prefix then suffix) to match `p2id::new`'s signature + # [target_id_suffix, target_id_prefix, tag, note_type, SERIAL_NUM]. + mem_load.CREATOR_PREFIX_PTR + mem_load.CREATOR_SUFFIX_PTR + # => [creator_suffix, creator_prefix, tag, note_type, s0', s1, s2, s3] + + exec.p2id::new + # => [note_idx] + end # => [note_idx] # --- move requested asset into the payback P2ID note diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index 5bd5d3e559..04d8f4c137 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -74,6 +74,13 @@ impl SwapNote { /// that is willing to consume the note. The consumer will receive the `offered_asset` and /// will create a new P2ID note with `sender` as target, containing the `requested_asset`. /// + /// The shape of the SWAP note storage depends on `payback_note_type`: + /// - [`NoteType::Private`]: the payback recipient digest is precomputed off-chain and + /// embedded as an opaque value, so the SWAP consumer cannot learn who the payback targets + /// from the storage alone. + /// - [`NoteType::Public`]: the creator id is embedded in plaintext so that any consumer of + /// the payback note can reconstruct its recipient at consume time. + /// /// # Errors /// Returns an error if deserialization or compilation of the `SWAP` script fails. pub fn create( @@ -89,9 +96,24 @@ impl SwapNote { return Err(NoteError::other("requested asset same as offered asset")); } - let swap_storage = SwapNoteStorage::new(requested_asset, payback_note_type, sender); - let serial_num = rng.draw_word(); + + // The payback recipient is P2ID(sender) with serial = swap_serial + 1, in both modes. + let payback_serial_num = payback_serial_from_swap(serial_num); + let payback_recipient = P2idNoteStorage::new(sender).into_recipient(payback_serial_num); + let payback_assets = NoteAssets::new(vec![requested_asset])?; + let payback_note = NoteDetails::new(payback_assets, payback_recipient.clone()); + + let payback_tag = NoteTag::with_account_target(sender); + let swap_storage = match payback_note_type { + NoteType::Private => SwapNoteStorage::new_private( + requested_asset, + payback_recipient.digest(), + payback_tag, + ), + NoteType::Public => SwapNoteStorage::new_public(requested_asset, sender, payback_tag), + }; + let recipient = swap_storage.into_recipient(serial_num); // build the tag for the SWAP use case @@ -102,12 +124,6 @@ impl SwapNote { let assets = NoteAssets::new(vec![offered_asset])?; let note = Note::with_attachments(assets, metadata, recipient, swap_note_attachments); - // build the payback note details - let payback_serial_num = payback_serial_from_swap(serial_num); - let payback_recipient = P2idNoteStorage::new(sender).into_recipient(payback_serial_num); - let payback_assets = NoteAssets::new(vec![requested_asset])?; - let payback_note = NoteDetails::new(payback_assets, payback_recipient); - Ok((note, payback_note)) } @@ -156,21 +172,44 @@ impl SwapNote { /// Canonical storage representation for a SWAP note. /// -/// Maps to the 11-element [`NoteStorage`] layout consumed by the on-chain MASM script: +/// Maps to the 16-element [`NoteStorage`] layout consumed by the on-chain MASM script: /// -/// | Slot | Field | -/// |----------|-------| -/// | `[0-7]` | Requested asset (key + value) | -/// | `[8]` | Payback note type (0 = private, 1 = public) | -/// | `[9-10]` | Creator account ID (prefix, suffix) | +/// | Slot | Field | +/// |-----------|-------| +/// | `[0..7]` | Requested asset (key + value) | +/// | `[8..11]` | Payback recipient digest (private mode; zero in public mode) | +/// | `[12]` | Payback note type | +/// | `[13]` | Payback note tag | +/// | `[14]` | Creator account ID prefix (public mode; zero in private mode) | +/// | `[15]` | Creator account ID suffix (public mode; zero in private mode) | /// -/// The payback note tag is derived at runtime from the creator account ID -/// (via `note_tag::create_account_target` in MASM). +/// In private mode the payback recipient digest is stored as an opaque value, so the consumer of +/// the SWAP cannot learn who the payback targets from the storage alone. In public mode the +/// creator id is stored in plaintext so the MASM can derive the payback recipient at consume time +/// via `p2id::new`. The payback note tag is stored explicitly in both modes; the creator is +/// responsible for picking one that targets the payback receiver when the payback is public. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SwapNoteStorage { - payback_note_type: NoteType, requested_asset: Asset, - creator_account_id: AccountId, + payback_tag: NoteTag, + payback: SwapPayback, +} + +/// Mode-specific payback data embedded in [`SwapNoteStorage`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SwapPayback { + /// Private payback: the recipient digest is precomputed off-chain by the creator and stored + /// as an opaque value. + Private { + /// Precomputed P2ID recipient digest for the payback note. + recipient: Word, + }, + /// Public payback: the creator id is stored in plaintext so the consumer can reconstruct + /// the payback recipient at consume time. + Public { + /// Account ID of the SWAP creator (the payback receiver). + creator_account_id: AccountId, + }, } impl SwapNoteStorage { @@ -178,27 +217,53 @@ impl SwapNoteStorage { // -------------------------------------------------------------------------------------------- /// Expected number of storage items of the SWAP note. - pub const NUM_ITEMS: usize = 11; + pub const NUM_ITEMS: usize = 16; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates new SWAP note storage with the specified parameters. - pub fn new( + /// Creates a new SWAP note storage for a private payback. + /// + /// `payback_recipient` is the precomputed P2ID recipient digest for the payback note and + /// `payback_tag` is the tag that the payback note will be created with. + pub fn new_private( + requested_asset: Asset, + payback_recipient: Word, + payback_tag: NoteTag, + ) -> Self { + Self { + requested_asset, + payback_tag, + payback: SwapPayback::Private { recipient: payback_recipient }, + } + } + + /// Creates a new SWAP note storage for a public payback. + /// + /// `creator_account_id` is embedded in plaintext so the consumer can reconstruct the payback + /// recipient. `payback_tag` is the tag attached to the payback note; it should target the + /// payback receiver so the network can route the note to it. + pub fn new_public( requested_asset: Asset, - payback_note_type: NoteType, creator_account_id: AccountId, + payback_tag: NoteTag, ) -> Self { Self { - payback_note_type, requested_asset, - creator_account_id, + payback_tag, + payback: SwapPayback::Public { creator_account_id }, } } - /// Returns the payback note type. + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the payback note type implied by the payback variant. pub fn payback_note_type(&self) -> NoteType { - self.payback_note_type + match self.payback { + SwapPayback::Private { .. } => NoteType::Private, + SwapPayback::Public { .. } => NoteType::Public, + } } /// Returns the requested asset. @@ -206,9 +271,14 @@ impl SwapNoteStorage { self.requested_asset } - /// Returns the creator account ID embedded in the SWAP storage. - pub fn creator_account_id(&self) -> AccountId { - self.creator_account_id + /// Returns the tag attached to the payback note. + pub fn payback_tag(&self) -> NoteTag { + self.payback_tag + } + + /// Returns the payback variant of this storage. + pub fn payback(&self) -> &SwapPayback { + &self.payback } /// Consumes the storage and returns a SWAP [`NoteRecipient`] with the provided serial number. @@ -223,17 +293,40 @@ impl SwapNoteStorage { impl From for NoteStorage { fn from(storage: SwapNoteStorage) -> Self { let mut storage_values = Vec::with_capacity(SwapNoteStorage::NUM_ITEMS); + + // [0..7] requested asset storage_values.extend_from_slice(&storage.requested_asset.as_elements()); - storage_values.push(Felt::from(storage.payback_note_type.as_u8())); - storage_values.push(storage.creator_account_id.prefix().as_felt()); - storage_values.push(storage.creator_account_id.suffix()); + + match storage.payback { + SwapPayback::Private { recipient } => { + // [8..11] payback recipient digest + storage_values.extend_from_slice(recipient.as_elements()); + // [12] payback note type + storage_values.push(Felt::from(NoteType::Private.as_u8())); + // [13] payback tag + storage_values.push(Felt::from(storage.payback_tag.as_u32())); + // [14..15] creator id (zero in private mode) + storage_values.extend_from_slice(&[Felt::ZERO; 2]); + }, + SwapPayback::Public { creator_account_id } => { + // [8..11] payback recipient (zero in public mode) + storage_values.extend_from_slice(&[Felt::ZERO; 4]); + // [12] payback note type + storage_values.push(Felt::from(NoteType::Public.as_u8())); + // [13] payback tag + storage_values.push(Felt::from(storage.payback_tag.as_u32())); + // [14..15] creator id (prefix, suffix) + storage_values.push(creator_account_id.prefix().as_felt()); + storage_values.push(creator_account_id.suffix()); + }, + } NoteStorage::new(storage_values) .expect("number of storage items should not exceed max storage items") } } -/// Deserializes [`SwapNoteStorage`] from a slice of exactly 11 [`Felt`]s. +/// Deserializes [`SwapNoteStorage`] from a slice of exactly 16 [`Felt`]s. impl TryFrom<&[Felt]> for SwapNoteStorage { type Error = NoteError; @@ -251,22 +344,59 @@ impl TryFrom<&[Felt]> for SwapNoteStorage { let requested_asset = Asset::from_key_value_words(key, value) .map_err(|e| NoteError::other_with_source("failed to parse requested asset", e))?; - // [8] = payback_note_type + // [12] = payback_note_type let payback_note_type = NoteType::try_from( - u8::try_from(note_storage[8].as_canonical_u64()) + u8::try_from(note_storage[12].as_canonical_u64()) .map_err(|_| NoteError::other("payback_note_type exceeds u8"))?, ) .map_err(|e| NoteError::other_with_source("failed to parse payback note type", e))?; - // [9..10] = creator account ID (prefix, suffix) - let creator_account_id = AccountId::try_from_elements(note_storage[10], note_storage[9]) - .map_err(|e| NoteError::other_with_source("failed to parse creator account ID", e))?; - - Ok(Self { - payback_note_type, - requested_asset, - creator_account_id, - }) + // [13] = payback tag (used in both modes) + let payback_tag_u32 = u32::try_from(note_storage[13].as_canonical_u64()) + .map_err(|_| NoteError::other("SWAP payback_tag exceeds u32"))?; + let payback_tag = NoteTag::new(payback_tag_u32); + + let payback = match payback_note_type { + NoteType::Private => { + // [14..15] must be zero so a private SWAP cannot leak a creator id. + if note_storage[14].as_canonical_u64() != 0 + || note_storage[15].as_canonical_u64() != 0 + { + return Err(NoteError::other( + "SWAP private payback must have creator id slots cleared", + )); + } + + // [8..11] payback recipient digest + let recipient = Word::new([ + note_storage[8], + note_storage[9], + note_storage[10], + note_storage[11], + ]); + + SwapPayback::Private { recipient } + }, + NoteType::Public => { + // [8..11] must be zero so the storage shape is unambiguous. + for slot in 8..=11 { + if note_storage[slot].as_canonical_u64() != 0 { + return Err(NoteError::other( + "SWAP public payback must have recipient slots cleared", + )); + } + } + + let creator_account_id = + AccountId::try_from_elements(note_storage[15], note_storage[14]).map_err( + |e| NoteError::other_with_source("failed to parse creator account ID", e), + )?; + + SwapPayback::Public { creator_account_id } + }, + }; + + Ok(Self { requested_asset, payback_tag, payback }) } } @@ -323,33 +453,76 @@ mod tests { ) } + fn dummy_recipient_digest() -> Word { + Word::new([Felt::from(7u32), Felt::from(11u32), Felt::from(13u32), Felt::from(17u32)]) + } + + fn dummy_payback_tag() -> NoteTag { + NoteTag::new(0xabcd1234) + } + #[test] - fn swap_note_storage_round_trip_fungible() { - let creator = dummy_creator_id(); - let storage = SwapNoteStorage::new(fungible_asset(), NoteType::Private, creator); + fn swap_note_storage_round_trip_fungible_private() { + let storage = SwapNoteStorage::new_private( + fungible_asset(), + dummy_recipient_digest(), + dummy_payback_tag(), + ); let note_storage = NoteStorage::from(storage.clone()); assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS); assert_eq!(storage.payback_note_type(), NoteType::Private); assert_eq!(storage.requested_asset(), fungible_asset()); - assert_eq!(storage.creator_account_id(), creator); + assert_eq!(storage.payback_tag(), dummy_payback_tag()); + match storage.payback() { + SwapPayback::Private { recipient } => { + assert_eq!(*recipient, dummy_recipient_digest()); + }, + SwapPayback::Public { .. } => panic!("expected private payback"), + } } #[test] fn swap_note_storage_round_trip_non_fungible_public() { let creator = dummy_creator_id(); - let storage = SwapNoteStorage::new(non_fungible_asset(), NoteType::Public, creator); + let storage = + SwapNoteStorage::new_public(non_fungible_asset(), creator, dummy_payback_tag()); let note_storage = NoteStorage::from(storage.clone()); assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS); assert_eq!(storage.payback_note_type(), NoteType::Public); assert_eq!(storage.requested_asset(), non_fungible_asset()); - assert_eq!(storage.creator_account_id(), creator); + assert_eq!(storage.payback_tag(), dummy_payback_tag()); + match storage.payback() { + SwapPayback::Public { creator_account_id } => { + assert_eq!(*creator_account_id, creator); + }, + SwapPayback::Private { .. } => panic!("expected public payback"), + } + } + + #[test] + fn swap_note_storage_try_from_round_trip_public() { + let original = SwapNoteStorage::new_public( + fungible_asset(), + dummy_creator_id(), + dummy_payback_tag(), + ); + let note_storage = NoteStorage::from(original.clone()); + + let parsed = + SwapNoteStorage::try_from(note_storage.items()).expect("round trip should succeed"); + + assert_eq!(parsed, original); } #[test] - fn swap_note_storage_try_from_round_trip() { - let original = SwapNoteStorage::new(fungible_asset(), NoteType::Public, dummy_creator_id()); + fn swap_note_storage_try_from_round_trip_private() { + let original = SwapNoteStorage::new_private( + fungible_asset(), + dummy_recipient_digest(), + dummy_payback_tag(), + ); let note_storage = NoteStorage::from(original.clone()); let parsed = @@ -358,6 +531,36 @@ mod tests { assert_eq!(parsed, original); } + #[test] + fn swap_note_storage_private_rejects_dirty_creator_slots() { + let mut items: Vec = NoteStorage::from(SwapNoteStorage::new_private( + fungible_asset(), + dummy_recipient_digest(), + dummy_payback_tag(), + )) + .items() + .to_vec(); + + // Inject a non-zero creator prefix in the slot that must stay clear for private payback. + items[14] = Felt::from(1u32); + assert!(SwapNoteStorage::try_from(items.as_slice()).is_err()); + } + + #[test] + fn swap_note_storage_public_rejects_dirty_private_slots() { + let mut items: Vec = NoteStorage::from(SwapNoteStorage::new_public( + fungible_asset(), + dummy_creator_id(), + dummy_payback_tag(), + )) + .items() + .to_vec(); + + // Inject a non-zero recipient felt in the slot that must stay clear for public payback. + items[8] = Felt::from(1u32); + assert!(SwapNoteStorage::try_from(items.as_slice()).is_err()); + } + #[test] fn swap_tag() { // Construct an ID that starts with 0xcdb1. From 4571ef97357f5becd915cff021691d06c552108f Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 18 May 2026 18:45:56 -0300 Subject: [PATCH 04/11] fix(standards): clippy and rustfmt in swap.rs --- crates/miden-standards/src/note/swap.rs | 27 ++++++++++--------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index 04d8f4c137..ce025da141 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -75,11 +75,11 @@ impl SwapNote { /// will create a new P2ID note with `sender` as target, containing the `requested_asset`. /// /// The shape of the SWAP note storage depends on `payback_note_type`: - /// - [`NoteType::Private`]: the payback recipient digest is precomputed off-chain and - /// embedded as an opaque value, so the SWAP consumer cannot learn who the payback targets - /// from the storage alone. - /// - [`NoteType::Public`]: the creator id is embedded in plaintext so that any consumer of - /// the payback note can reconstruct its recipient at consume time. + /// - [`NoteType::Private`]: the payback recipient digest is precomputed off-chain and embedded + /// as an opaque value, so the SWAP consumer cannot learn who the payback targets from the + /// storage alone. + /// - [`NoteType::Public`]: the creator id is embedded in plaintext so that any consumer of the + /// payback note can reconstruct its recipient at consume time. /// /// # Errors /// Returns an error if deserialization or compilation of the `SWAP` script fails. @@ -379,12 +379,10 @@ impl TryFrom<&[Felt]> for SwapNoteStorage { }, NoteType::Public => { // [8..11] must be zero so the storage shape is unambiguous. - for slot in 8..=11 { - if note_storage[slot].as_canonical_u64() != 0 { - return Err(NoteError::other( - "SWAP public payback must have recipient slots cleared", - )); - } + if note_storage[8..=11].iter().any(|f| f.as_canonical_u64() != 0) { + return Err(NoteError::other( + "SWAP public payback must have recipient slots cleared", + )); } let creator_account_id = @@ -503,11 +501,8 @@ mod tests { #[test] fn swap_note_storage_try_from_round_trip_public() { - let original = SwapNoteStorage::new_public( - fungible_asset(), - dummy_creator_id(), - dummy_payback_tag(), - ); + let original = + SwapNoteStorage::new_public(fungible_asset(), dummy_creator_id(), dummy_payback_tag()); let note_storage = NoteStorage::from(original.clone()); let parsed = From 97c9a2e010a3183e150119ddd8b4d79114e1cab0 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 18 May 2026 19:16:19 -0300 Subject: [PATCH 05/11] docs: refresh SWAP storage description in note.md --- crates/miden-standards/src/note/swap.rs | 2 +- docs/src/note.md | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index ce025da141..d14ac9dd9f 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -351,7 +351,7 @@ impl TryFrom<&[Felt]> for SwapNoteStorage { ) .map_err(|e| NoteError::other_with_source("failed to parse payback note type", e))?; - // [13] = payback tag (used in both modes) + // [13] = payback tag let payback_tag_u32 = u32::try_from(note_storage[13].as_canonical_u64()) .map_err(|_| NoteError::other("SWAP payback_tag exceeds u32"))?; let payback_tag = NoteTag::new(payback_tag_u32); diff --git a/docs/src/note.md b/docs/src/note.md index 1b9004f835..12575d9098 100644 --- a/docs/src/note.md +++ b/docs/src/note.md @@ -228,14 +228,16 @@ The SWAP note script implements atomic asset swapping functionality. **Key characteristics:** - **Purpose:** Atomic asset exchange between two parties -- **Storage:** Requires exactly 11 storage items specifying: - - Requested asset +- **Storage:** Requires exactly 16 storage items, laid out as: + - Requested asset (8 felts) + - Payback recipient digest (4 felts; used for private payback, zero for public) - Payback note type - - Creator account ID prefix - - Creator account ID suffix + - Payback tag + - Creator account ID prefix (used for public payback, zero for private) + - Creator account ID suffix (used for public payback, zero for private) - **Assets:** Must contain exactly 1 asset to be swapped - **Mechanism:** - 1. Creates a payback note containing the requested asset for the original note issuer + 1. Creates a payback P2ID note containing the requested asset for the original note issuer. For private payback, the precomputed recipient digest is loaded from storage and used directly. For public payback, the recipient is reconstructed on-chain from the creator account ID and a serial derived as `swap_serial + 1`, which also registers the preimage in the advice map so the public note can be validated. 2. Adds the note's asset to the consuming account's vault - **Requirements:** Account must expose both: - `miden::standards::wallets::basic::receive_asset` procedure From 1bcb235230d81d6300f9ad78f3f025829517d883 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 19 May 2026 12:48:37 -0300 Subject: [PATCH 06/11] docs(swap): explain why creator id is stored explicitly --- crates/miden-standards/asm/standards/notes/swap.masm | 4 ++++ crates/miden-standards/src/note/swap.rs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/crates/miden-standards/asm/standards/notes/swap.masm b/crates/miden-standards/asm/standards/notes/swap.masm index 0ba35c594a..3da3f76f1e 100644 --- a/crates/miden-standards/asm/standards/notes/swap.masm +++ b/crates/miden-standards/asm/standards/notes/swap.masm @@ -25,6 +25,10 @@ const SWAP_NOTE_NUM_STORAGE_ITEMS=16 # creator id is embedded in plaintext and the MASM derives the recipient at consume time. The # payback tag is stored explicitly in both modes; the creator is responsible for picking a tag # that targets the payback receiver in public mode. +# +# The creator id could be derived from `active_note::get_sender` since today creator == sender, +# but it's stored explicitly because this slot is meant to represent an arbitrary payback target +# (not necessarily the creator) in a future iteration. const STORAGE_PTR=0 const REQUESTED_ASSET_PTR=0 const PAYBACK_RECIPIENT_PTR=8 diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index d14ac9dd9f..ac21d445cf 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -206,6 +206,9 @@ pub enum SwapPayback { }, /// Public payback: the creator id is stored in plaintext so the consumer can reconstruct /// the payback recipient at consume time. + /// + /// Stored explicitly rather than derived from the SWAP sender to leave room for + /// third-party paybacks in the future. Public { /// Account ID of the SWAP creator (the payback receiver). creator_account_id: AccountId, From ae52be2b5b75b8d19984477fc686b29f026056af Mon Sep 17 00:00:00 2001 From: JereSalo Date: Tue, 19 May 2026 13:38:36 -0300 Subject: [PATCH 07/11] rename SWAP creator id field to payback target id --- .../asm/standards/notes/swap.masm | 41 +++++----- crates/miden-standards/src/note/swap.rs | 75 ++++++++++--------- docs/src/note.md | 6 +- 3 files changed, 64 insertions(+), 58 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/swap.masm b/crates/miden-standards/asm/standards/notes/swap.masm index 3da3f76f1e..bdc5a999e2 100644 --- a/crates/miden-standards/asm/standards/notes/swap.masm +++ b/crates/miden-standards/asm/standards/notes/swap.masm @@ -11,31 +11,31 @@ use miden::standards::wallets::basic->wallet const SWAP_NOTE_NUM_STORAGE_ITEMS=16 # Note storage layout (16 felts, loaded at STORAGE_PTR by get_storage): -# - requested_asset_key [0..3] -# - requested_asset_value [4..7] -# - payback_recipient [8..11] (private mode only; zero in public mode) -# - payback_note_type [12] -# - payback_tag [13] -# - creator_id_prefix [14] (public mode only; zero in private mode) -# - creator_id_suffix [15] (public mode only; zero in private mode) +# - requested_asset_key [0..3] +# - requested_asset_value [4..7] +# - payback_recipient [8..11] (private mode only; zero in public mode) +# - payback_note_type [12] +# - payback_tag [13] +# - payback_target_prefix [14] (public mode only; zero in private mode) +# - payback_target_suffix [15] (public mode only; zero in private mode) # # In private mode, the recipient digest and tag are precomputed off-chain by the creator and # embedded as opaque values; the consumer of the SWAP cannot learn who the payback targets from # storage alone. In public mode, the recipient must be reconstructible by any consumer, so the -# creator id is embedded in plaintext and the MASM derives the recipient at consume time. The -# payback tag is stored explicitly in both modes; the creator is responsible for picking a tag -# that targets the payback receiver in public mode. +# payback target account id is embedded in plaintext and the MASM derives the recipient at +# consume time. The payback tag is stored explicitly in both modes; the creator is responsible +# for picking a tag that targets the payback receiver in public mode. # -# The creator id could be derived from `active_note::get_sender` since today creator == sender, -# but it's stored explicitly because this slot is meant to represent an arbitrary payback target -# (not necessarily the creator) in a future iteration. +# The payback target id could be derived from `active_note::get_sender` since today the target +# equals the SWAP sender, but it's stored explicitly because this slot is meant to represent an +# arbitrary payback target (not necessarily the creator) in a future iteration. const STORAGE_PTR=0 const REQUESTED_ASSET_PTR=0 const PAYBACK_RECIPIENT_PTR=8 const PAYBACK_NOTE_TYPE_PTR=12 const PAYBACK_TAG_PTR=13 -const CREATOR_PREFIX_PTR=14 -const CREATOR_SUFFIX_PTR=15 +const PAYBACK_TARGET_PREFIX_PTR=14 +const PAYBACK_TARGET_SUFFIX_PTR=15 # Storage is exactly 16 felts = 4 words, so the asset region starts right after. const ASSET_PTR=16 @@ -51,7 +51,8 @@ const ERR_SWAP_WRONG_NUMBER_OF_ASSETS="SWAP script requires exactly 1 note asset #! P2ID payback note addressed to the creator carrying the requested asset. #! #! The payback note type (selected by the creator) determines how the payback recipient is -#! derived: private uses an opaque precomputed digest, public derives it from the creator id. +#! derived: private uses an opaque precomputed digest, public derives it from the payback target +#! account id stored in plaintext. #! #! Requires that the account exposes: #! - miden::standards::wallets::basic::receive_asset procedure. @@ -110,11 +111,11 @@ pub proc main mem_load.PAYBACK_TAG_PTR # => [tag, note_type, s0', s1, s2, s3] - # Load creator id (prefix then suffix) to match `p2id::new`'s signature + # Load payback target id (prefix then suffix) to match `p2id::new`'s signature # [target_id_suffix, target_id_prefix, tag, note_type, SERIAL_NUM]. - mem_load.CREATOR_PREFIX_PTR - mem_load.CREATOR_SUFFIX_PTR - # => [creator_suffix, creator_prefix, tag, note_type, s0', s1, s2, s3] + mem_load.PAYBACK_TARGET_PREFIX_PTR + mem_load.PAYBACK_TARGET_SUFFIX_PTR + # => [target_suffix, target_prefix, tag, note_type, s0', s1, s2, s3] exec.p2id::new # => [note_idx] diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index ac21d445cf..2538aa4e0e 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -78,8 +78,8 @@ impl SwapNote { /// - [`NoteType::Private`]: the payback recipient digest is precomputed off-chain and embedded /// as an opaque value, so the SWAP consumer cannot learn who the payback targets from the /// storage alone. - /// - [`NoteType::Public`]: the creator id is embedded in plaintext so that any consumer of the - /// payback note can reconstruct its recipient at consume time. + /// - [`NoteType::Public`]: the payback target account id is embedded in plaintext so that any + /// consumer of the payback note can reconstruct its recipient at consume time. /// /// # Errors /// Returns an error if deserialization or compilation of the `SWAP` script fails. @@ -180,14 +180,15 @@ impl SwapNote { /// | `[8..11]` | Payback recipient digest (private mode; zero in public mode) | /// | `[12]` | Payback note type | /// | `[13]` | Payback note tag | -/// | `[14]` | Creator account ID prefix (public mode; zero in private mode) | -/// | `[15]` | Creator account ID suffix (public mode; zero in private mode) | +/// | `[14]` | Payback target account ID prefix (public mode; zero in private mode) | +/// | `[15]` | Payback target account ID suffix (public mode; zero in private mode) | /// /// In private mode the payback recipient digest is stored as an opaque value, so the consumer of /// the SWAP cannot learn who the payback targets from the storage alone. In public mode the -/// creator id is stored in plaintext so the MASM can derive the payback recipient at consume time -/// via `p2id::new`. The payback note tag is stored explicitly in both modes; the creator is -/// responsible for picking one that targets the payback receiver when the payback is public. +/// payback target account id is stored in plaintext so the MASM can derive the payback recipient +/// at consume time via `p2id::new`. The payback note tag is stored explicitly in both modes; the +/// creator is responsible for picking one that targets the payback receiver when the payback is +/// public. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SwapNoteStorage { requested_asset: Asset, @@ -204,14 +205,15 @@ pub enum SwapPayback { /// Precomputed P2ID recipient digest for the payback note. recipient: Word, }, - /// Public payback: the creator id is stored in plaintext so the consumer can reconstruct - /// the payback recipient at consume time. + /// Public payback: the payback target account id is stored in plaintext so the consumer can + /// reconstruct the payback recipient at consume time. /// /// Stored explicitly rather than derived from the SWAP sender to leave room for /// third-party paybacks in the future. Public { - /// Account ID of the SWAP creator (the payback receiver). - creator_account_id: AccountId, + /// Account ID of the payback receiver. Today this is the SWAP creator by convention, but + /// the script does not enforce it: it can be any account in a future iteration. + payback_target_id: AccountId, }, } @@ -243,18 +245,18 @@ impl SwapNoteStorage { /// Creates a new SWAP note storage for a public payback. /// - /// `creator_account_id` is embedded in plaintext so the consumer can reconstruct the payback + /// `payback_target_id` is embedded in plaintext so the consumer can reconstruct the payback /// recipient. `payback_tag` is the tag attached to the payback note; it should target the /// payback receiver so the network can route the note to it. pub fn new_public( requested_asset: Asset, - creator_account_id: AccountId, + payback_target_id: AccountId, payback_tag: NoteTag, ) -> Self { Self { requested_asset, payback_tag, - payback: SwapPayback::Public { creator_account_id }, + payback: SwapPayback::Public { payback_target_id }, } } @@ -308,19 +310,19 @@ impl From for NoteStorage { storage_values.push(Felt::from(NoteType::Private.as_u8())); // [13] payback tag storage_values.push(Felt::from(storage.payback_tag.as_u32())); - // [14..15] creator id (zero in private mode) + // [14..15] payback target id (zero in private mode) storage_values.extend_from_slice(&[Felt::ZERO; 2]); }, - SwapPayback::Public { creator_account_id } => { + SwapPayback::Public { payback_target_id } => { // [8..11] payback recipient (zero in public mode) storage_values.extend_from_slice(&[Felt::ZERO; 4]); // [12] payback note type storage_values.push(Felt::from(NoteType::Public.as_u8())); // [13] payback tag storage_values.push(Felt::from(storage.payback_tag.as_u32())); - // [14..15] creator id (prefix, suffix) - storage_values.push(creator_account_id.prefix().as_felt()); - storage_values.push(creator_account_id.suffix()); + // [14..15] payback target id (prefix, suffix) + storage_values.push(payback_target_id.prefix().as_felt()); + storage_values.push(payback_target_id.suffix()); }, } @@ -361,12 +363,12 @@ impl TryFrom<&[Felt]> for SwapNoteStorage { let payback = match payback_note_type { NoteType::Private => { - // [14..15] must be zero so a private SWAP cannot leak a creator id. + // [14..15] must be zero so a private SWAP cannot leak a payback target id. if note_storage[14].as_canonical_u64() != 0 || note_storage[15].as_canonical_u64() != 0 { return Err(NoteError::other( - "SWAP private payback must have creator id slots cleared", + "SWAP private payback must have payback target id slots cleared", )); } @@ -388,12 +390,15 @@ impl TryFrom<&[Felt]> for SwapNoteStorage { )); } - let creator_account_id = - AccountId::try_from_elements(note_storage[15], note_storage[14]).map_err( - |e| NoteError::other_with_source("failed to parse creator account ID", e), - )?; + let payback_target_id = AccountId::try_from_elements( + note_storage[15], + note_storage[14], + ) + .map_err(|e| { + NoteError::other_with_source("failed to parse payback target account ID", e) + })?; - SwapPayback::Public { creator_account_id } + SwapPayback::Public { payback_target_id } }, }; @@ -445,7 +450,7 @@ mod tests { Asset::NonFungible(NonFungibleAsset::new(&details).unwrap()) } - fn dummy_creator_id() -> AccountId { + fn dummy_target_id() -> AccountId { AccountId::dummy( [1; 15], AccountIdVersion::Version1, @@ -485,9 +490,9 @@ mod tests { #[test] fn swap_note_storage_round_trip_non_fungible_public() { - let creator = dummy_creator_id(); + let target = dummy_target_id(); let storage = - SwapNoteStorage::new_public(non_fungible_asset(), creator, dummy_payback_tag()); + SwapNoteStorage::new_public(non_fungible_asset(), target, dummy_payback_tag()); let note_storage = NoteStorage::from(storage.clone()); assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS); @@ -495,8 +500,8 @@ mod tests { assert_eq!(storage.requested_asset(), non_fungible_asset()); assert_eq!(storage.payback_tag(), dummy_payback_tag()); match storage.payback() { - SwapPayback::Public { creator_account_id } => { - assert_eq!(*creator_account_id, creator); + SwapPayback::Public { payback_target_id } => { + assert_eq!(*payback_target_id, target); }, SwapPayback::Private { .. } => panic!("expected public payback"), } @@ -505,7 +510,7 @@ mod tests { #[test] fn swap_note_storage_try_from_round_trip_public() { let original = - SwapNoteStorage::new_public(fungible_asset(), dummy_creator_id(), dummy_payback_tag()); + SwapNoteStorage::new_public(fungible_asset(), dummy_target_id(), dummy_payback_tag()); let note_storage = NoteStorage::from(original.clone()); let parsed = @@ -530,7 +535,7 @@ mod tests { } #[test] - fn swap_note_storage_private_rejects_dirty_creator_slots() { + fn swap_note_storage_private_rejects_dirty_target_slots() { let mut items: Vec = NoteStorage::from(SwapNoteStorage::new_private( fungible_asset(), dummy_recipient_digest(), @@ -539,7 +544,7 @@ mod tests { .items() .to_vec(); - // Inject a non-zero creator prefix in the slot that must stay clear for private payback. + // Inject a non-zero target prefix in the slot that must stay clear for private payback. items[14] = Felt::from(1u32); assert!(SwapNoteStorage::try_from(items.as_slice()).is_err()); } @@ -548,7 +553,7 @@ mod tests { fn swap_note_storage_public_rejects_dirty_private_slots() { let mut items: Vec = NoteStorage::from(SwapNoteStorage::new_public( fungible_asset(), - dummy_creator_id(), + dummy_target_id(), dummy_payback_tag(), )) .items() diff --git a/docs/src/note.md b/docs/src/note.md index 12575d9098..b8477441de 100644 --- a/docs/src/note.md +++ b/docs/src/note.md @@ -233,11 +233,11 @@ The SWAP note script implements atomic asset swapping functionality. - Payback recipient digest (4 felts; used for private payback, zero for public) - Payback note type - Payback tag - - Creator account ID prefix (used for public payback, zero for private) - - Creator account ID suffix (used for public payback, zero for private) + - Payback target account ID prefix (used for public payback, zero for private) + - Payback target account ID suffix (used for public payback, zero for private) - **Assets:** Must contain exactly 1 asset to be swapped - **Mechanism:** - 1. Creates a payback P2ID note containing the requested asset for the original note issuer. For private payback, the precomputed recipient digest is loaded from storage and used directly. For public payback, the recipient is reconstructed on-chain from the creator account ID and a serial derived as `swap_serial + 1`, which also registers the preimage in the advice map so the public note can be validated. + 1. Creates a payback P2ID note containing the requested asset for the original note issuer. For private payback, the precomputed recipient digest is loaded from storage and used directly. For public payback, the recipient is reconstructed on-chain from the payback target account ID and a serial derived as `swap_serial + 1`, which also registers the preimage in the advice map so the public note can be validated. 2. Adds the note's asset to the consuming account's vault - **Requirements:** Account must expose both: - `miden::standards::wallets::basic::receive_asset` procedure From 97858e0707eec0b7cd633822195d72c4359ff451 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Wed, 20 May 2026 17:32:49 -0300 Subject: [PATCH 08/11] docs(swap): consolidate per-mode payback docs Centralize the rationale for the SwapPayback::Private vs Public storage shape on the enum itself and drop the repeated explanations from SwapNote::create, SwapNoteStorage, and the constructors. Also remove a stale comment from swap.masm. --- .../asm/standards/notes/swap.masm | 1 - crates/miden-standards/src/note/swap.rs | 38 +++++-------------- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/crates/miden-standards/asm/standards/notes/swap.masm b/crates/miden-standards/asm/standards/notes/swap.masm index bdc5a999e2..2347e8fd96 100644 --- a/crates/miden-standards/asm/standards/notes/swap.masm +++ b/crates/miden-standards/asm/standards/notes/swap.masm @@ -102,7 +102,6 @@ pub proc main else # --- PUBLIC PAYBACK --- # Derive P2ID serial = SWAP_SERIAL with the least significant element +1. - # The creator can recompute this offline from SWAP's own serial number. exec.active_note::get_serial_number add.1 # => [s0', s1, s2, s3] diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index 2538aa4e0e..e7da2a0d70 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -19,10 +19,10 @@ use miden_protocol::note::{ PartialNoteMetadata, }; use miden_protocol::utils::sync::LazyLock; -use miden_protocol::{Felt, ONE, Word}; +use miden_protocol::{Felt, Word, ONE}; -use crate::StandardsLib; use crate::note::P2idNoteStorage; +use crate::StandardsLib; // NOTE SCRIPT // ================================================================================================ @@ -74,12 +74,7 @@ impl SwapNote { /// that is willing to consume the note. The consumer will receive the `offered_asset` and /// will create a new P2ID note with `sender` as target, containing the `requested_asset`. /// - /// The shape of the SWAP note storage depends on `payback_note_type`: - /// - [`NoteType::Private`]: the payback recipient digest is precomputed off-chain and embedded - /// as an opaque value, so the SWAP consumer cannot learn who the payback targets from the - /// storage alone. - /// - [`NoteType::Public`]: the payback target account id is embedded in plaintext so that any - /// consumer of the payback note can reconstruct its recipient at consume time. + /// See [`SwapPayback`] for how the two payback modes shape the SWAP note storage. /// /// # Errors /// Returns an error if deserialization or compilation of the `SWAP` script fails. @@ -183,12 +178,7 @@ impl SwapNote { /// | `[14]` | Payback target account ID prefix (public mode; zero in private mode) | /// | `[15]` | Payback target account ID suffix (public mode; zero in private mode) | /// -/// In private mode the payback recipient digest is stored as an opaque value, so the consumer of -/// the SWAP cannot learn who the payback targets from the storage alone. In public mode the -/// payback target account id is stored in plaintext so the MASM can derive the payback recipient -/// at consume time via `p2id::new`. The payback note tag is stored explicitly in both modes; the -/// creator is responsible for picking one that targets the payback receiver when the payback is -/// public. +/// See [`SwapPayback`] for the rationale behind the per-mode shape. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SwapNoteStorage { requested_asset: Asset, @@ -197,19 +187,18 @@ pub struct SwapNoteStorage { } /// Mode-specific payback data embedded in [`SwapNoteStorage`]. +/// +/// The variant determines how the payback recipient is materialized at consume time: +/// - [`SwapPayback::Private`] embeds the precomputed P2ID recipient digest as an opaque value, so +/// the SWAP storage alone does not reveal who the payback targets. +/// - [`SwapPayback::Public`] embeds the payback target account id in plaintext, so any consumer +/// can reconstruct the payback recipient at consume time via `p2id::new`. #[derive(Debug, Clone, PartialEq, Eq)] pub enum SwapPayback { - /// Private payback: the recipient digest is precomputed off-chain by the creator and stored - /// as an opaque value. Private { /// Precomputed P2ID recipient digest for the payback note. recipient: Word, }, - /// Public payback: the payback target account id is stored in plaintext so the consumer can - /// reconstruct the payback recipient at consume time. - /// - /// Stored explicitly rather than derived from the SWAP sender to leave room for - /// third-party paybacks in the future. Public { /// Account ID of the payback receiver. Today this is the SWAP creator by convention, but /// the script does not enforce it: it can be any account in a future iteration. @@ -228,9 +217,6 @@ impl SwapNoteStorage { // -------------------------------------------------------------------------------------------- /// Creates a new SWAP note storage for a private payback. - /// - /// `payback_recipient` is the precomputed P2ID recipient digest for the payback note and - /// `payback_tag` is the tag that the payback note will be created with. pub fn new_private( requested_asset: Asset, payback_recipient: Word, @@ -244,10 +230,6 @@ impl SwapNoteStorage { } /// Creates a new SWAP note storage for a public payback. - /// - /// `payback_target_id` is embedded in plaintext so the consumer can reconstruct the payback - /// recipient. `payback_tag` is the tag attached to the payback note; it should target the - /// payback receiver so the network can route the note to it. pub fn new_public( requested_asset: Asset, payback_target_id: AccountId, From 82c70d10ed5b973d0e7c766628bab8e8c1b6d1d2 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Wed, 20 May 2026 17:38:04 -0300 Subject: [PATCH 09/11] style(standards): rustfmt swap.rs --- crates/miden-standards/src/note/swap.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index e7da2a0d70..b295a43bb8 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -19,10 +19,10 @@ use miden_protocol::note::{ PartialNoteMetadata, }; use miden_protocol::utils::sync::LazyLock; -use miden_protocol::{Felt, Word, ONE}; +use miden_protocol::{Felt, ONE, Word}; -use crate::note::P2idNoteStorage; use crate::StandardsLib; +use crate::note::P2idNoteStorage; // NOTE SCRIPT // ================================================================================================ @@ -191,8 +191,8 @@ pub struct SwapNoteStorage { /// The variant determines how the payback recipient is materialized at consume time: /// - [`SwapPayback::Private`] embeds the precomputed P2ID recipient digest as an opaque value, so /// the SWAP storage alone does not reveal who the payback targets. -/// - [`SwapPayback::Public`] embeds the payback target account id in plaintext, so any consumer -/// can reconstruct the payback recipient at consume time via `p2id::new`. +/// - [`SwapPayback::Public`] embeds the payback target account id in plaintext, so any consumer can +/// reconstruct the payback recipient at consume time via `p2id::new`. #[derive(Debug, Clone, PartialEq, Eq)] pub enum SwapPayback { Private { From 5b38771f12b753bfdd69842f6daced1a5ecfca5a Mon Sep 17 00:00:00 2001 From: JereSalo Date: Wed, 20 May 2026 17:40:50 -0300 Subject: [PATCH 10/11] test(standards): adapt dummy_target_id to new AccountId::dummy signature --- crates/miden-standards/src/note/swap.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index fd75ce24dc..0ad249acd7 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -432,12 +432,7 @@ mod tests { } fn dummy_target_id() -> AccountId { - AccountId::dummy( - [1; 15], - AccountIdVersion::Version1, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Public, - ) + AccountId::dummy([1; 15], AccountIdVersion::Version1, AccountType::Public) } fn dummy_recipient_digest() -> Word { From f30e3de39fd04b0aa8020b686ee82aacf0ec70c9 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Wed, 20 May 2026 17:48:20 -0300 Subject: [PATCH 11/11] feat(standards): re-export SwapPayback from note module --- crates/miden-standards/src/note/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-standards/src/note/mod.rs b/crates/miden-standards/src/note/mod.rs index e42145305d..a0c58404ea 100644 --- a/crates/miden-standards/src/note/mod.rs +++ b/crates/miden-standards/src/note/mod.rs @@ -25,7 +25,7 @@ mod pswap; pub use pswap::{PswapNote, PswapNoteStorage}; mod swap; -pub use swap::{SwapNote, SwapNoteStorage, payback_serial_from_swap}; +pub use swap::{SwapNote, SwapNoteStorage, SwapPayback, payback_serial_from_swap}; mod network_account_target; pub use network_account_target::{NetworkAccountTarget, NetworkAccountTargetError};