diff --git a/miden-lib/asm/scripts/SWAP.masm b/miden-lib/asm/scripts/SWAP.masm new file mode 100644 index 000000000..db75d252d --- /dev/null +++ b/miden-lib/asm/scripts/SWAP.masm @@ -0,0 +1,56 @@ +use.miden::sat::note +use.miden::wallets::basic->wallet + +# Swap script: adds an asset from the note into consumers account and +# creates a note consumable by note issuer containing requested ASSET. +# +# Requires that the account exposes: +# +# Inputs: [SCRIPT_ROOT] +# Outputs: [] +# +# Note inputs are assumed to be as follows: +# - RECIPIENT +# - ASSET +# - TAG = [tag, 0, 0, 0] +# +# FAILS if: +# - Account does not expose miden::wallets::basic::receive_asset procedure +# - Account does not expose miden::wallets::basic::send_asset procedure +# - 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 +begin + # drop the transaction script root + dropw + # => [] + + # store asset into memory at address 3 + push.3 exec.note::get_assets assert + # => [ptr] + + # load the asset and add it to the account + mem_loadw call.wallet::receive_asset dropw + # => [] + + # store note inputs into memory starting at address 0 + push.0 exec.note::get_inputs + # => [inputs_ptr] + + # load recipient + drop padw mem_loadw + # => [RECIPIENT] + + padw mem_loadw.1 + # => [ASSET, RECIPIENT] + + padw mem_loadw.2 + # => [0, 0, 0, tag, ASSET, RECIPIENT] + + drop drop drop movdn.4 + # => [ASSET, tag, RECIPIENT] + + # create a note using inputs + call.wallet::send_asset dropw dropw + # => [] +end \ No newline at end of file diff --git a/miden-lib/src/notes/mod.rs b/miden-lib/src/notes/mod.rs index 62ef0cc0b..1f175af24 100644 --- a/miden-lib/src/notes/mod.rs +++ b/miden-lib/src/notes/mod.rs @@ -9,7 +9,7 @@ use miden_objects::{ assets::Asset, notes::{Note, NoteMetadata, NoteScript, NoteStub, NoteVault}, utils::{collections::Vec, vec}, - Digest, Felt, NoteError, StarkField, Word, WORD_SIZE, ZERO, + Digest, Felt, Hasher, NoteError, StarkField, Word, WORD_SIZE, ZERO, }; pub enum Script { @@ -20,11 +20,16 @@ pub enum Script { target: AccountId, recall_height: u32, }, + SWAP { + asset: Asset, + serial_num: Word, + }, } -/// Users can create notes with a standard script. Atm we provide two standard scripts: +/// Users can create notes with a standard script. Atm we provide three standard scripts: /// 1. P2ID - pay to id. /// 2. P2IDR - pay to id with recall after a certain block height. +/// 3. SWAP - swap of assets between two accounts. pub fn create_note( script: Script, assets: Vec, @@ -37,6 +42,7 @@ pub fn create_note( // Include the binary version of the scripts into the source file at compile time let p2id_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/P2ID.masb")); let p2idr_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/P2IDR.masb")); + let swap_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/SWAP.masb")); let (note_script_ast, inputs): (ProgramAst, Vec) = match script { Script::P2ID { target } => ( @@ -50,6 +56,27 @@ pub fn create_note( ProgramAst::from_bytes(p2idr_bytes).map_err(NoteError::NoteDeserializationError)?, vec![target.into(), recall_height.into(), ZERO, ZERO], ), + Script::SWAP { asset, serial_num } => { + let recipient = build_p2id_recipient(sender, serial_num)?; + let asset_word: Word = asset.into(); + ( + ProgramAst::from_bytes(swap_bytes).map_err(NoteError::NoteDeserializationError)?, + vec![ + recipient[0], + recipient[1], + recipient[2], + recipient[3], + asset_word[0], + asset_word[1], + asset_word[2], + asset_word[3], + sender.into(), + ZERO, + ZERO, + ZERO, + ], + ) + } }; let (note_script, _) = NoteScript::new(note_script_ast, ¬e_assembler)?; @@ -88,3 +115,28 @@ pub fn notes_try_from_elements(elements: &[Word]) -> Result Ok(stub) } + +/// Utility function generating RECIPIENT for the P2ID note script created by the SWAP script +fn build_p2id_recipient(target: AccountId, serial_num: Word) -> Result { + // TODO: add lazy_static initialization or compile-time optimization instead of re-generating + // the script hash every time we call the SWAP script + let assembler = assembler(); + + let p2id_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/P2ID.masb")); + + let note_script_ast = + ProgramAst::from_bytes(p2id_bytes).map_err(NoteError::NoteDeserializationError)?; + + let (note_script, _) = NoteScript::new(note_script_ast, &assembler)?; + + let script_hash = note_script.hash(); + + let serial_num_hash = Hasher::merge(&[serial_num.into(), Digest::default()]); + + let merge_script = Hasher::merge(&[serial_num_hash, script_hash]); + + Ok(Hasher::merge(&[ + merge_script, + Hasher::hash_elements(&[target.into(), ZERO, ZERO, ZERO]), + ])) +} diff --git a/miden-tx/tests/test_miden_swap_script.rs b/miden-tx/tests/test_miden_swap_script.rs new file mode 100644 index 000000000..3d7f5c346 --- /dev/null +++ b/miden-tx/tests/test_miden_swap_script.rs @@ -0,0 +1,128 @@ +use common::{ + get_account_with_default_account_code, get_new_key_pair_with_advice_map, MockDataStore, +}; +use miden_lib::notes::{create_note, Script}; +use miden_objects::{ + accounts::{Account, AccountId, AccountVault, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN}, + assembly::ProgramAst, + assets::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}, + notes::{NoteMetadata, NoteStub, NoteVault}, + Felt, +}; +use miden_tx::TransactionExecutor; +use mock::constants::{ + ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN, + ACCOUNT_ID_SENDER, +}; +use vm_processor::Digest; + +mod common; + +#[test] +fn test_swap_script() { + // Create assets + let faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); + let fungible_asset: Asset = FungibleAsset::new(faucet_id, 100).unwrap().into(); + + let faucet_id_2 = AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); + let non_fungible_asset: Asset = NonFungibleAsset::new( + &NonFungibleAssetDetails::new(faucet_id_2, vec![1, 2, 3, 4]).unwrap(), + ) + .unwrap() + .into(); + + // Create sender and target account + let sender_account_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap(); + + let target_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN).unwrap(); + let (target_pub_key, target_sk_felt) = get_new_key_pair_with_advice_map(); + let target_account = get_account_with_default_account_code( + target_account_id, + target_pub_key.clone(), + Some(non_fungible_asset), + ); + + // Create the note + let aswap_script = Script::SWAP { + asset: non_fungible_asset, + serial_num: [Felt::new(6), Felt::new(7), Felt::new(8), Felt::new(9)], + }; + + let note = create_note( + aswap_script, + vec![fungible_asset], + sender_account_id, + None, + [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)], + ) + .unwrap(); + + // CONSTRUCT AND EXECUTE TX (Success) + // -------------------------------------------------------------------------------------------- + let data_store = + MockDataStore::with_existing(Some(target_account.clone()), Some(vec![note.clone()]), None); + + let mut executor = TransactionExecutor::new(data_store.clone()); + executor.load_account(target_account_id).unwrap(); + + let block_ref = data_store.block_header.block_num(); + let note_origins = + data_store.notes.iter().map(|note| note.origin().clone()).collect::>(); + + let tx_script_code = ProgramAst::parse( + format!( + " + use.miden::auth::basic->auth_tx + + begin + call.auth_tx::auth_tx_rpo_falcon512 + end + " + ) + .as_str(), + ) + .unwrap(); + let tx_script_target = executor + .compile_tx_script(tx_script_code.clone(), vec![(target_pub_key, target_sk_felt)], vec![]) + .unwrap(); + + // Execute the transaction + let transaction_result = executor + .execute_transaction(target_account_id, block_ref, ¬e_origins, Some(tx_script_target)) + .unwrap(); + + // target account vault delta + let target_account_after: Account = Account::new( + target_account.id(), + AccountVault::new(&vec![fungible_asset]).unwrap(), + target_account.storage().clone(), + target_account.code().clone(), + Felt::new(2), + ); + + // Check that the target account has received the asset from the note + assert!(transaction_result.final_account_hash() == target_account_after.hash()); + + // Check if only one `Note` has been created + assert!(transaction_result.created_notes().notes().len() == 1); + + // Check if the created `Note` is what we expect + let recipient = Digest::new([ + Felt::new(403044469077705077), + Felt::new(5814218301633521607), + Felt::new(3036312160134047413), + Felt::new(9100684949500007517), + ]); + + let note_metadata = + NoteMetadata::new(target_account_id, sender_account_id.into(), Felt::new(1)); + + let note_vault = NoteVault::new(&[non_fungible_asset]).unwrap(); + + let requested_note = NoteStub::new(recipient, note_vault, note_metadata).unwrap(); + + let created_note = &transaction_result.created_notes().notes()[0]; + + assert!(created_note == &requested_note); +}