Skip to content

Commit

Permalink
Merge pull request #363 from phklive/aswap
Browse files Browse the repository at this point in the history
Add `Swap` script
  • Loading branch information
phklive authored Dec 21, 2023
2 parents 72e255c + 54d17eb commit d901d51
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 2 deletions.
56 changes: 56 additions & 0 deletions miden-lib/asm/scripts/SWAP.masm
Original file line number Diff line number Diff line change
@@ -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
56 changes: 54 additions & 2 deletions miden-lib/src/notes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<Asset>,
Expand All @@ -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<Felt>) = match script {
Script::P2ID { target } => (
Expand All @@ -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, &note_assembler)?;
Expand Down Expand Up @@ -88,3 +115,28 @@ pub fn notes_try_from_elements(elements: &[Word]) -> Result<NoteStub, NoteError>

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<Digest, NoteError> {
// 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]),
]))
}
128 changes: 128 additions & 0 deletions miden-tx/tests/test_miden_swap_script.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<_>>();

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, &note_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);
}

0 comments on commit d901d51

Please sign in to comment.