use alloc::string::String;
use miden_protocol::account::auth::AuthScheme;
use miden_protocol::account::{Account, AccountId};
use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset};
use miden_protocol::crypto::rand::RandomCoin;
use miden_protocol::errors::tx_kernel::{
ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS,
ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT,
};
use miden_protocol::note::{
Note,
NoteAttachment,
NoteAttachmentScheme,
NoteMetadata,
NoteRecipient,
NoteStorage,
NoteTag,
NoteType,
};
use miden_protocol::testing::account_id::{
ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET,
ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
ACCOUNT_ID_PRIVATE_SENDER,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE,
ACCOUNT_ID_SENDER,
};
use miden_protocol::testing::constants::NON_FUNGIBLE_ASSET_DATA_2;
use miden_protocol::transaction::memory::{
ASSET_SIZE,
ASSET_VALUE_OFFSET,
NOTE_MEM_SIZE,
NUM_OUTPUT_NOTES_PTR,
OUTPUT_NOTE_ASSETS_OFFSET,
OUTPUT_NOTE_ATTACHMENT_OFFSET,
OUTPUT_NOTE_METADATA_HEADER_OFFSET,
OUTPUT_NOTE_NUM_ASSETS_OFFSET,
OUTPUT_NOTE_RECIPIENT_OFFSET,
OUTPUT_NOTE_SECTION_OFFSET,
};
use miden_protocol::transaction::{RawOutputNote, RawOutputNotes};
use miden_protocol::{Felt, Word, ZERO};
use miden_standards::code_builder::CodeBuilder;
use miden_standards::note::{
AccountTargetNetworkNote,
NetworkAccountTarget,
NetworkNoteExt,
NoteExecutionHint,
P2idNote,
};
use miden_standards::testing::mock_account::MockAccountExt;
use miden_standards::testing::note::NoteBuilder;
use super::{TestSetup, setup_test};
use crate::kernel_tests::tx::ExecutionOutputExt;
use crate::utils::{create_public_p2any_note, create_spawn_note};
use crate::{Auth, MockChain, TransactionContextBuilder, assert_execution_error};
#[tokio::test]
async fn test_create_note() -> anyhow::Result<()> {
let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?;
let account_id = tx_context.account().id();
let recipient = Word::from([0, 1, 2, 3u32]);
let tag = NoteTag::with_account_target(account_id);
let code = format!(
"
use miden::protocol::output_note
use $kernel::prologue
begin
exec.prologue::prepare_transaction
push.{recipient}
push.{PUBLIC_NOTE}
push.{tag}
exec.output_note::create
# truncate the stack
swapdw dropw dropw
end
",
recipient = recipient,
PUBLIC_NOTE = NoteType::Public as u8,
tag = tag,
);
let exec_output = &tx_context.execute_code(&code).await?;
assert_eq!(
exec_output.get_kernel_mem_element(NUM_OUTPUT_NOTES_PTR),
Felt::from(1u32),
"number of output notes must increment by 1",
);
assert_eq!(
exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_RECIPIENT_OFFSET),
recipient,
"recipient must be stored at the correct memory location",
);
let metadata = NoteMetadata::new(account_id, NoteType::Public).with_tag(tag);
let expected_metadata_header = metadata.to_header_word();
let expected_note_attachment = metadata.to_attachment_word();
assert_eq!(
exec_output
.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_HEADER_OFFSET),
expected_metadata_header,
"metadata header must be stored at the correct memory location",
);
assert_eq!(
exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_OFFSET),
expected_note_attachment,
"attachment must be stored at the correct memory location",
);
assert_eq!(
exec_output.get_stack_element(0),
ZERO,
"top item on the stack is the index of the output note"
);
Ok(())
}
#[tokio::test]
async fn test_create_note_with_invalid_tag() -> anyhow::Result<()> {
let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?;
let invalid_tag = Felt::new((NoteType::Public as u64) << 62);
let valid_tag: Felt = NoteTag::default().into();
assert!(tx_context.execute_code(¬e_creation_script(invalid_tag)).await.is_err());
assert!(tx_context.execute_code(¬e_creation_script(valid_tag)).await.is_ok());
Ok(())
}
fn note_creation_script(tag: Felt) -> String {
format!(
"
use miden::protocol::output_note
use $kernel::prologue
begin
exec.prologue::prepare_transaction
push.{recipient}
push.{PUBLIC_NOTE}
push.{tag}
exec.output_note::create
# clean the stack
dropw dropw
end
",
recipient = Word::from([0, 1, 2, 3u32]),
PUBLIC_NOTE = NoteType::Public as u8,
)
}
#[tokio::test]
async fn test_create_note_too_many_notes() -> anyhow::Result<()> {
let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?;
let code = format!(
"
use miden::protocol::output_note
use $kernel::constants::MAX_OUTPUT_NOTES_PER_TX
use $kernel::memory
use $kernel::prologue
begin
push.MAX_OUTPUT_NOTES_PER_TX
exec.memory::set_num_output_notes
exec.prologue::prepare_transaction
push.{recipient}
push.{PUBLIC_NOTE}
push.{tag}
exec.output_note::create
end
",
tag = NoteTag::new(1234 << 16 | 5678),
recipient = Word::from([0, 1, 2, 3u32]),
PUBLIC_NOTE = NoteType::Public as u8,
);
let exec_output = tx_context.execute_code(&code).await;
assert_execution_error!(exec_output, ERR_TX_NUMBER_OF_OUTPUT_NOTES_EXCEEDS_LIMIT);
Ok(())
}
#[tokio::test]
async fn test_get_output_notes_commitment() -> anyhow::Result<()> {
let mut rng = RandomCoin::new(Word::from([1, 2, 3, 4u32]));
let account = Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce);
let asset_1 = FungibleAsset::mock(100);
let asset_2 = FungibleAsset::mock(200);
let input_note_1 = create_public_p2any_note(ACCOUNT_ID_PRIVATE_SENDER.try_into()?, [asset_1]);
let input_note_2 = create_public_p2any_note(ACCOUNT_ID_PRIVATE_SENDER.try_into()?, [asset_2]);
let output_note_1 = NoteBuilder::new(account.id(), &mut rng)
.tag(NoteTag::with_account_target(account.id()).as_u32())
.note_type(NoteType::Public)
.add_assets([asset_1])
.build()?;
let output_note_2 = NoteBuilder::new(account.id(), &mut rng)
.tag(NoteTag::with_custom_account_target(account.id(), 2)?.as_u32())
.note_type(NoteType::Public)
.add_assets([asset_2])
.attachment(NoteAttachment::new_array(
NoteAttachmentScheme::new(5),
[42, 43, 44, 45, 46u32].map(Felt::from).to_vec(),
)?)
.build()?;
let tx_context = TransactionContextBuilder::new(account)
.extend_input_notes(vec![input_note_1.clone(), input_note_2.clone()])
.extend_expected_output_notes(vec![
RawOutputNote::Full(output_note_1.clone()),
RawOutputNote::Full(output_note_2.clone()),
])
.build()?;
let expected_output_notes_commitment = RawOutputNotes::new(vec![
RawOutputNote::Full(output_note_1.clone()),
RawOutputNote::Full(output_note_2.clone()),
])?
.commitment();
let code = format!(
"
use miden::core::sys
use miden::protocol::tx
use miden::protocol::output_note
use $kernel::prologue
begin
exec.prologue::prepare_transaction
# => []
# create output note 1
push.{recipient_1}
push.{PUBLIC_NOTE}
push.{tag_1}
exec.output_note::create
# => [note_idx]
push.{ASSET_1_VALUE}
push.{ASSET_1_KEY}
exec.output_note::add_asset
# => []
# create output note 2
push.{recipient_2}
push.{PUBLIC_NOTE}
push.{tag_2}
exec.output_note::create
# => [note_idx]
dup
push.{ASSET_2_VALUE}
push.{ASSET_2_KEY}
exec.output_note::add_asset
# => [note_idx]
push.{ATTACHMENT2}
push.{attachment_scheme2}
movup.5
# => [note_idx, attachment_scheme, ATTACHMENT]
exec.output_note::set_array_attachment
# => []
# compute the output notes commitment
exec.tx::get_output_notes_commitment
# => [OUTPUT_NOTES_COMMITMENT]
# truncate the stack
exec.sys::truncate_stack
# => [OUTPUT_NOTES_COMMITMENT]
end
",
PUBLIC_NOTE = NoteType::Public as u8,
recipient_1 = output_note_1.recipient().digest(),
tag_1 = output_note_1.metadata().tag(),
ASSET_1_KEY = asset_1.to_key_word(),
ASSET_1_VALUE = asset_1.to_value_word(),
recipient_2 = output_note_2.recipient().digest(),
tag_2 = output_note_2.metadata().tag(),
ASSET_2_KEY = asset_2.to_key_word(),
ASSET_2_VALUE = asset_2.to_value_word(),
ATTACHMENT2 = output_note_2.metadata().to_attachment_word(),
attachment_scheme2 = output_note_2.metadata().attachment().attachment_scheme().as_u32(),
);
let exec_output = &tx_context.execute_code(&code).await?;
assert_eq!(
exec_output.get_kernel_mem_element(NUM_OUTPUT_NOTES_PTR),
Felt::from(2u32),
"The test creates two notes",
);
assert_eq!(
exec_output
.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_HEADER_OFFSET),
output_note_1.metadata().to_header_word(),
"Validate the output note 1 metadata header",
);
assert_eq!(
exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_OFFSET),
output_note_1.metadata().to_attachment_word(),
"Validate the output note 1 attachment",
);
assert_eq!(
exec_output.get_kernel_mem_word(
OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_METADATA_HEADER_OFFSET + NOTE_MEM_SIZE
),
output_note_2.metadata().to_header_word(),
"Validate the output note 2 metadata header",
);
assert_eq!(
exec_output.get_kernel_mem_word(
OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ATTACHMENT_OFFSET + NOTE_MEM_SIZE
),
output_note_2.metadata().to_attachment_word(),
"Validate the output note 2 attachment",
);
assert_eq!(exec_output.get_stack_word(0), expected_output_notes_commitment);
Ok(())
}
#[tokio::test]
async fn test_create_note_and_add_asset() -> anyhow::Result<()> {
let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?;
let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?;
let recipient = Word::from([0, 1, 2, 3u32]);
let tag = NoteTag::with_account_target(faucet_id);
let asset = FungibleAsset::new(faucet_id, 10)?;
let code = format!(
"
use miden::protocol::output_note
use $kernel::prologue
begin
exec.prologue::prepare_transaction
push.{recipient}
push.{PUBLIC_NOTE}
push.{tag}
exec.output_note::create
# => [note_idx]
# assert that the index of the created note equals zero
dup assertz.err=\"index of the created note should be zero\"
# => [note_idx]
push.{ASSET_VALUE}
push.{ASSET_KEY}
# => [ASSET_KEY, ASSET_VALUE, note_idx]
call.output_note::add_asset
# => []
# truncate the stack
dropw dropw dropw
end
",
recipient = recipient,
PUBLIC_NOTE = NoteType::Public as u8,
tag = tag,
ASSET_KEY = asset.to_key_word(),
ASSET_VALUE = asset.to_value_word(),
);
let exec_output = &tx_context.execute_code(&code).await?;
assert_eq!(
exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET),
asset.to_key_word(),
"asset key must be stored at the correct memory location",
);
assert_eq!(
exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET + 4),
asset.to_value_word(),
"asset value must be stored at the correct memory location",
);
Ok(())
}
#[tokio::test]
async fn test_create_note_and_add_multiple_assets() -> anyhow::Result<()> {
let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?;
let faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?;
let faucet_2 = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2)?;
let recipient = Word::from([0, 1, 2, 3u32]);
let tag = NoteTag::with_account_target(faucet_2);
let asset = FungibleAsset::new(faucet, 10)?;
let asset_2 = FungibleAsset::new(faucet_2, 20)?;
let asset_3 = FungibleAsset::new(faucet_2, 30)?;
let asset_2_plus_3 = FungibleAsset::new(faucet_2, 50)?;
let non_fungible_asset = NonFungibleAsset::mock(&NON_FUNGIBLE_ASSET_DATA_2);
let code = format!(
"
use miden::protocol::output_note
use $kernel::prologue
begin
exec.prologue::prepare_transaction
push.{recipient}
push.{PUBLIC_NOTE}
push.{tag}
exec.output_note::create
# => [note_idx]
# assert that the index of the created note equals zero
dup assertz.err=\"index of the created note should be zero\"
# => [note_idx]
dup
push.{ASSET_VALUE}
push.{ASSET_KEY}
exec.output_note::add_asset
# => [note_idx]
dup
push.{ASSET2_VALUE}
push.{ASSET2_KEY}
exec.output_note::add_asset
# => [note_idx]
dup
push.{ASSET3_VALUE}
push.{ASSET3_KEY}
exec.output_note::add_asset
# => [note_idx]
push.{ASSET4_VALUE}
push.{ASSET4_KEY}
exec.output_note::add_asset
# => []
# truncate the stack
repeat.7 dropw end
end
",
recipient = recipient,
PUBLIC_NOTE = NoteType::Public as u8,
tag = tag,
ASSET_KEY = asset.to_key_word(),
ASSET_VALUE = asset.to_value_word(),
ASSET2_KEY = asset_2.to_key_word(),
ASSET2_VALUE = asset_2.to_value_word(),
ASSET3_KEY = asset_3.to_key_word(),
ASSET3_VALUE = asset_3.to_value_word(),
ASSET4_KEY = non_fungible_asset.to_key_word(),
ASSET4_VALUE = non_fungible_asset.to_value_word(),
);
let exec_output = &tx_context.execute_code(&code).await?;
assert_eq!(
exec_output
.get_kernel_mem_element(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_NUM_ASSETS_OFFSET)
.as_canonical_u64(),
3,
"unexpected number of assets in output note",
);
assert_eq!(
exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET),
asset.to_key_word(),
"asset key must be stored at the correct memory location",
);
assert_eq!(
exec_output.get_kernel_mem_word(
OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET + ASSET_VALUE_OFFSET
),
asset.to_value_word(),
"asset value must be stored at the correct memory location",
);
assert_eq!(
exec_output.get_kernel_mem_word(
OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET + ASSET_SIZE
),
asset_2_plus_3.to_key_word(),
"asset key must be stored at the correct memory location",
);
assert_eq!(
exec_output.get_kernel_mem_word(
OUTPUT_NOTE_SECTION_OFFSET
+ OUTPUT_NOTE_ASSETS_OFFSET
+ ASSET_SIZE
+ ASSET_VALUE_OFFSET
),
asset_2_plus_3.to_value_word(),
"asset value must be stored at the correct memory location",
);
assert_eq!(
exec_output.get_kernel_mem_word(
OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_ASSETS_OFFSET + ASSET_SIZE * 2
),
non_fungible_asset.to_key_word(),
"asset key must be stored at the correct memory location",
);
assert_eq!(
exec_output.get_kernel_mem_word(
OUTPUT_NOTE_SECTION_OFFSET
+ OUTPUT_NOTE_ASSETS_OFFSET
+ ASSET_SIZE * 2
+ ASSET_VALUE_OFFSET
),
non_fungible_asset.to_value_word(),
"asset value must be stored at the correct memory location",
);
Ok(())
}
#[tokio::test]
async fn test_create_note_and_add_same_nft_twice() -> anyhow::Result<()> {
let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?;
let recipient = Word::from([0, 1, 2, 3u32]);
let tag = NoteTag::new(999 << 16 | 777);
let non_fungible_asset = NonFungibleAsset::mock(&[1, 2, 3]);
let code = format!(
"
use $kernel::prologue
use miden::protocol::output_note
begin
exec.prologue::prepare_transaction
# => []
push.{recipient}
push.{PUBLIC_NOTE}
push.{tag}
exec.output_note::create
# => [note_idx]
dup
push.{ASSET_VALUE}
push.{ASSET_KEY}
# => [ASSET_KEY, ASSET_VALUE, note_idx, note_idx]
exec.output_note::add_asset
# => [note_idx]
push.{ASSET_VALUE}
push.{ASSET_KEY}
exec.output_note::add_asset
# => []
end
",
recipient = recipient,
PUBLIC_NOTE = NoteType::Public as u8,
tag = tag,
ASSET_KEY = non_fungible_asset.to_key_word(),
ASSET_VALUE = non_fungible_asset.to_value_word(),
);
let exec_output = tx_context.execute_code(&code).await;
assert_execution_error!(exec_output, ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS);
Ok(())
}
#[tokio::test]
async fn creating_note_with_fungible_asset_amount_zero_works() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
let output_note = builder.add_p2id_note(
account.id(),
account.id(),
&[FungibleAsset::mock(0)],
NoteType::Private,
)?;
let input_note = builder.add_spawn_note([&output_note])?;
let chain = builder.build()?;
chain
.build_tx_context(account, &[input_note.id()], &[])?
.build()?
.execute()
.await?;
Ok(())
}
#[tokio::test]
async fn test_build_recipient_hash() -> anyhow::Result<()> {
let tx_context = {
let account =
Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce);
let input_note_1 = create_public_p2any_note(
ACCOUNT_ID_SENDER.try_into().unwrap(),
[FungibleAsset::mock(100)],
);
TransactionContextBuilder::new(account)
.extend_input_notes(vec![input_note_1])
.build()?
};
let input_note_1 = tx_context.tx_inputs().input_notes().get_note(0).note();
let output_serial_no = Word::from([0, 1, 2, 3u32]);
let tag = NoteTag::new(42 << 16 | 42);
let single_input = 2;
let storage = NoteStorage::new(vec![Felt::new(single_input)]).unwrap();
let storage_commitment = storage.commitment();
let recipient = NoteRecipient::new(output_serial_no, input_note_1.script().clone(), storage);
let code = format!(
"
use $kernel::prologue
use miden::protocol::output_note
use miden::protocol::note
use miden::core::sys
begin
exec.prologue::prepare_transaction
# storage
push.{storage_commitment}
# SCRIPT_ROOT
push.{script_root}
# SERIAL_NUM
push.{output_serial_no}
# => [SERIAL_NUM, SCRIPT_ROOT, STORAGE_COMMITMENT]
exec.note::build_recipient_hash
# => [RECIPIENT, pad(12)]
push.{PUBLIC_NOTE}
push.{tag}
# => [tag, note_type, RECIPIENT]
exec.output_note::create
# => [note_idx]
# clean the stack
exec.sys::truncate_stack
end
",
script_root = input_note_1.script().clone().root(),
output_serial_no = output_serial_no,
PUBLIC_NOTE = NoteType::Public as u8,
tag = tag,
);
let exec_output = &tx_context.execute_code(&code).await?;
assert_eq!(
exec_output.get_kernel_mem_element(NUM_OUTPUT_NOTES_PTR),
Felt::from(1u32),
"number of output notes must increment by 1",
);
let recipient_digest = recipient.clone().digest();
assert_eq!(
exec_output.get_kernel_mem_word(OUTPUT_NOTE_SECTION_OFFSET + OUTPUT_NOTE_RECIPIENT_OFFSET),
recipient_digest,
"recipient hash not correct",
);
Ok(())
}
#[tokio::test]
async fn test_get_asset_info() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let fungible_asset_0 = Asset::Fungible(
FungibleAsset::new(
AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).expect("id should be valid"),
5,
)
.expect("asset is invalid"),
);
let fungible_asset_1 = Asset::Fungible(
FungibleAsset::new(
AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).expect("id should be valid"),
5,
)
.expect("asset is invalid"),
);
let account = builder.add_existing_wallet_with_assets(
Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
},
[fungible_asset_0, fungible_asset_1],
)?;
let mock_chain = builder.build()?;
let output_note_0 = P2idNote::create(
account.id(),
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?,
vec![fungible_asset_0],
NoteType::Public,
NoteAttachment::default(),
&mut RandomCoin::new(Word::from([1, 2, 3, 4u32])),
)?;
let output_note_1 = P2idNote::create(
account.id(),
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?,
vec![fungible_asset_0, fungible_asset_1],
NoteType::Public,
NoteAttachment::default(),
&mut RandomCoin::new(Word::from([4, 3, 2, 1u32])),
)?;
let tx_script_src = &format!(
r#"
use miden::protocol::output_note
use miden::core::sys
begin
# create an output note with fungible asset 0
push.{RECIPIENT}
push.{note_type}
push.{tag}
exec.output_note::create
# => [note_idx]
# move the asset 0 to the note
dup
push.{ASSET_0_VALUE}
push.{ASSET_0_KEY}
call.::miden::standards::wallets::basic::move_asset_to_note
# => [note_idx]
# get the assets hash and assets number of the note having only asset_0
dup exec.output_note::get_assets_info
# => [ASSETS_COMMITMENT_0, num_assets_0, note_idx]
# assert the correctness of the assets hash
push.{COMPUTED_ASSETS_COMMITMENT_0}
assert_eqw.err="assets commitment of the note having only asset_0 is incorrect"
# => [num_assets_0, note_idx]
# assert the number of assets
push.{assets_number_0}
assert_eq.err="number of assets in the note having only asset_0 is incorrect"
# => [note_idx]
# get the assets info once more to get the cached data and assert that this data didn't
# change
dup exec.output_note::get_assets_info
push.{COMPUTED_ASSETS_COMMITMENT_0}
assert_eqw.err="assets commitment of the note having only asset_0 is incorrect"
push.{assets_number_0}
assert_eq.err="number of assets in the note having only asset_0 is incorrect"
# => [note_idx]
# add asset_1 to the note
dup
push.{ASSET_1_VALUE}
push.{ASSET_1_KEY}
call.::miden::standards::wallets::basic::move_asset_to_note
# => [note_idx]
# get the assets hash and assets number of the note having asset_0 and asset_1
dup exec.output_note::get_assets_info
# => [ASSETS_COMMITMENT_1, num_assets_1, note_idx]
# assert the correctness of the assets hash
push.{COMPUTED_ASSETS_COMMITMENT_1}
assert_eqw.err="assets commitment of the note having asset_0 and asset_1 is incorrect"
# => [num_assets_1, note_idx]
# assert the number of assets
push.{assets_number_1}
assert_eq.err="number of assets in the note having asset_0 and asset_1 is incorrect"
# => [note_idx]
# truncate the stack
exec.sys::truncate_stack
end
"#,
RECIPIENT = output_note_1.recipient().digest(),
note_type = NoteType::Public as u8,
tag = output_note_1.metadata().tag(),
ASSET_0_VALUE = fungible_asset_0.to_value_word(),
ASSET_0_KEY = fungible_asset_0.to_key_word(),
COMPUTED_ASSETS_COMMITMENT_0 = output_note_0.assets().commitment(),
assets_number_0 = output_note_0.assets().num_assets(),
ASSET_1_VALUE = fungible_asset_1.to_value_word(),
ASSET_1_KEY = fungible_asset_1.to_key_word(),
COMPUTED_ASSETS_COMMITMENT_1 = output_note_1.assets().commitment(),
assets_number_1 = output_note_1.assets().num_assets(),
);
let tx_script = CodeBuilder::default().compile_tx_script(tx_script_src)?;
let tx_context = mock_chain
.build_tx_context(account.id(), &[], &[])?
.extend_expected_output_notes(vec![RawOutputNote::Full(output_note_1)])
.tx_script(tx_script)
.build()?;
tx_context.execute().await?;
Ok(())
}
#[tokio::test]
async fn test_get_recipient_and_metadata() -> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account = builder.add_existing_wallet_with_assets(
Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
},
[FungibleAsset::mock(2000)],
)?;
let mock_chain = builder.build()?;
let output_note = P2idNote::create(
account.id(),
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?,
vec![FungibleAsset::mock(5)],
NoteType::Public,
NoteAttachment::default(),
&mut RandomCoin::new(Word::from([1, 2, 3, 4u32])),
)?;
let tx_script_src = &format!(
r#"
use miden::protocol::output_note
use miden::core::sys
begin
# create an output note with one asset
{output_note} drop
# => []
# get the recipient (the only existing note has 0'th index)
push.0
exec.output_note::get_recipient
# => [RECIPIENT]
# assert the correctness of the recipient
push.{RECIPIENT}
assert_eqw.err="requested note has incorrect recipient"
# => []
# get the metadata (the only existing note has 0'th index)
push.0
exec.output_note::get_metadata
# => [NOTE_ATTACHMENT, METADATA_HEADER]
push.{NOTE_ATTACHMENT}
assert_eqw.err="requested note has incorrect note attachment"
# => [METADATA_HEADER]
push.{METADATA_HEADER}
assert_eqw.err="requested note has incorrect metadata header"
# => []
# truncate the stack
exec.sys::truncate_stack
end
"#,
output_note = create_output_note(&output_note),
RECIPIENT = output_note.recipient().digest(),
METADATA_HEADER = output_note.metadata().to_header_word(),
NOTE_ATTACHMENT = output_note.metadata().to_attachment_word(),
);
let tx_script = CodeBuilder::default().compile_tx_script(tx_script_src)?;
let tx_context = mock_chain
.build_tx_context(account.id(), &[], &[])?
.extend_expected_output_notes(vec![RawOutputNote::Full(output_note)])
.tx_script(tx_script)
.build()?;
tx_context.execute().await?;
Ok(())
}
#[tokio::test]
async fn test_get_assets() -> anyhow::Result<()> {
let TestSetup {
mock_chain,
account,
p2id_note_0_assets,
p2id_note_1_asset,
p2id_note_2_assets,
} = setup_test()?;
fn check_assets_code(note_index: u8, dest_ptr: u8, note: &Note) -> String {
let mut check_assets_code = format!(
r#"
# push the note index and memory destination pointer
push.{note_idx} push.{dest_ptr}
# => [dest_ptr, note_index]
# write the assets to memory
exec.output_note::get_assets
# => [num_assets, dest_ptr, note_index]
# assert the number of note assets
push.{assets_number}
assert_eq.err="expected note {note_index} to have {assets_number} assets"
# => [dest_ptr, note_index]
"#,
note_idx = note_index,
dest_ptr = dest_ptr,
assets_number = note.assets().num_assets(),
);
for (asset_index, asset) in note.assets().iter().enumerate() {
check_assets_code.push_str(&format!(
r#"
# load the asset stored in memory
padw dup.4 mem_loadw_le
# => [STORED_ASSET_KEY, dest_ptr, note_index]
# assert the asset key matches
push.{NOTE_ASSET_KEY}
assert_eqw.err="expected asset key at asset index {asset_index} of the note\
{note_index} to be {NOTE_ASSET_KEY}"
# => [dest_ptr, note_index]
# load the asset stored in memory
padw dup.4 add.{ASSET_VALUE_OFFSET} mem_loadw_le
# => [STORED_ASSET_VALUE, dest_ptr, note_index]
# assert the asset value matches
push.{NOTE_ASSET_VALUE}
assert_eqw.err="expected asset value at asset index {asset_index} of the note\
{note_index} to be {NOTE_ASSET_VALUE}"
# => [dest_ptr, note_index]
# move the pointer
add.{ASSET_SIZE}
# => [dest_ptr+ASSET_SIZE, note_index]
"#,
NOTE_ASSET_KEY = asset.to_key_word(),
NOTE_ASSET_VALUE = asset.to_value_word(),
asset_index = asset_index,
note_index = note_index,
));
}
check_assets_code.push_str("\ndrop drop");
check_assets_code
}
let tx_script_src = &format!(
"
use miden::protocol::output_note
use miden::core::sys
begin
{create_note_0}
{check_note_0}
{create_note_1}
{check_note_1}
{create_note_2}
{check_note_2}
# truncate the stack
exec.sys::truncate_stack
end
",
create_note_0 = create_output_note(&p2id_note_0_assets),
check_note_0 = check_assets_code(0, 0, &p2id_note_0_assets),
create_note_1 = create_output_note(&p2id_note_1_asset),
check_note_1 = check_assets_code(1, 8, &p2id_note_1_asset),
create_note_2 = create_output_note(&p2id_note_2_assets),
check_note_2 = check_assets_code(2, 16, &p2id_note_2_assets),
);
let tx_script = CodeBuilder::default().compile_tx_script(tx_script_src)?;
let tx_context = mock_chain
.build_tx_context(account.id(), &[], &[])?
.extend_expected_output_notes(vec![
RawOutputNote::Full(p2id_note_0_assets),
RawOutputNote::Full(p2id_note_1_asset),
RawOutputNote::Full(p2id_note_2_assets),
])
.tx_script(tx_script)
.build()?;
tx_context.execute().await?;
Ok(())
}
#[tokio::test]
async fn test_set_none_attachment() -> anyhow::Result<()> {
let account = Account::mock(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, Auth::IncrNonce);
let rng = RandomCoin::new(Word::from([1, 2, 3, 4u32]));
let attachment = NoteAttachment::default();
let output_note =
RawOutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?);
let tx_script = format!(
"
use miden::protocol::output_note
begin
push.{RECIPIENT}
push.{note_type}
push.{tag}
exec.output_note::create
# => [note_idx]
push.{ATTACHMENT}
push.{attachment_kind}
push.{attachment_scheme}
movup.6
# => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT]
exec.output_note::set_attachment
# => []
# truncate the stack
swapdw dropw dropw
end
",
RECIPIENT = output_note.recipient().unwrap().digest(),
note_type = output_note.metadata().note_type() as u8,
tag = output_note.metadata().tag().as_u32(),
ATTACHMENT = output_note.metadata().to_attachment_word(),
attachment_kind = output_note.metadata().attachment().content().attachment_kind().as_u8(),
attachment_scheme = output_note.metadata().attachment().attachment_scheme().as_u32(),
);
let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?;
let tx = TransactionContextBuilder::new(account)
.extend_expected_output_notes(vec![output_note.clone()])
.tx_script(tx_script)
.build()?
.execute()
.await?;
let actual_note = tx.output_notes().get_note(0);
assert_eq!(actual_note.header(), output_note.header());
assert_eq!(actual_note.assets(), output_note.assets());
Ok(())
}
#[tokio::test]
async fn test_set_word_attachment() -> anyhow::Result<()> {
let account = Account::mock(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, Auth::IncrNonce);
let rng = RandomCoin::new(Word::from([1, 2, 3, 4u32]));
let attachment =
NoteAttachment::new_word(NoteAttachmentScheme::new(u32::MAX), Word::from([3, 4, 5, 6u32]));
let output_note =
RawOutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?);
let tx_script = format!(
"
use miden::protocol::output_note
begin
push.{RECIPIENT}
push.{note_type}
push.{tag}
exec.output_note::create
# => [note_idx]
push.{ATTACHMENT}
push.{attachment_scheme}
movup.5
# => [note_idx, attachment_scheme, ATTACHMENT]
exec.output_note::set_word_attachment
# => []
# truncate the stack
swapdw dropw dropw
end
",
RECIPIENT = output_note.recipient().unwrap().digest(),
note_type = output_note.metadata().note_type() as u8,
tag = output_note.metadata().tag().as_u32(),
attachment_scheme = output_note.metadata().attachment().attachment_scheme().as_u32(),
ATTACHMENT = output_note.metadata().to_attachment_word(),
);
let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?;
let tx = TransactionContextBuilder::new(account)
.extend_expected_output_notes(vec![output_note.clone()])
.tx_script(tx_script)
.build()?
.execute()
.await?;
let actual_note = tx.output_notes().get_note(0);
assert_eq!(actual_note.header(), output_note.header());
assert_eq!(actual_note.assets(), output_note.assets());
Ok(())
}
#[tokio::test]
async fn test_set_array_attachment() -> anyhow::Result<()> {
let account = Account::mock(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, Auth::IncrNonce);
let rng = RandomCoin::new(Word::from([1, 2, 3, 4u32]));
let elements = [3, 4, 5, 6, 7, 8, 9u32].map(Felt::from).to_vec();
let attachment = NoteAttachment::new_array(NoteAttachmentScheme::new(42), elements.clone())?;
let output_note =
RawOutputNote::Full(NoteBuilder::new(account.id(), rng).attachment(attachment).build()?);
let tx_script = format!(
"
use miden::protocol::output_note
begin
push.{RECIPIENT}
push.{note_type}
push.{tag}
exec.output_note::create
# => [note_idx]
push.{ATTACHMENT}
push.{attachment_scheme}
movup.5
# => [note_idx, attachment_scheme, ATTACHMENT]
exec.output_note::set_array_attachment
# => []
# truncate the stack
swapdw dropw dropw
end
",
RECIPIENT = output_note.recipient().unwrap().digest(),
note_type = output_note.metadata().note_type() as u8,
tag = output_note.metadata().tag().as_u32(),
attachment_scheme = output_note.metadata().attachment().attachment_scheme().as_u32(),
ATTACHMENT = output_note.metadata().to_attachment_word(),
);
let tx_script = CodeBuilder::new().compile_tx_script(tx_script)?;
let tx = TransactionContextBuilder::new(account)
.extend_expected_output_notes(vec![output_note.clone()])
.tx_script(tx_script)
.extend_advice_map(vec![(output_note.metadata().to_attachment_word(), elements)])
.build()?
.execute()
.await?;
let actual_note = tx.output_notes().get_note(0);
assert_eq!(actual_note.header(), output_note.header());
assert_eq!(actual_note.assets(), output_note.assets());
Ok(())
}
#[tokio::test]
async fn test_set_network_target_account_attachment() -> anyhow::Result<()> {
let account = Account::mock(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, Auth::IncrNonce);
let rng = RandomCoin::new(Word::from([1, 2, 3, 4u32]));
let attachment = NetworkAccountTarget::new(
ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET.try_into()?,
NoteExecutionHint::on_block_slot(5, 32, 3),
)?;
let output_note = NoteBuilder::new(account.id(), rng)
.note_type(NoteType::Private)
.attachment(attachment)
.build()?;
let spawn_note = create_spawn_note([&output_note])?;
let tx = TransactionContextBuilder::new(account)
.extend_input_notes([spawn_note].to_vec())
.build()?
.execute()
.await?;
let actual_note = tx.output_notes().get_note(0);
assert_eq!(actual_note.header(), output_note.header());
assert_eq!(actual_note.assets(), output_note.assets());
let actual_attachment = NetworkAccountTarget::try_from(actual_note.metadata().attachment())?;
assert_eq!(actual_attachment, attachment);
Ok(())
}
#[tokio::test]
async fn test_network_note() -> anyhow::Result<()> {
let sender = Account::mock(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, Auth::IncrNonce);
let mut rng = RandomCoin::new(Word::from([9, 8, 7, 6u32]));
let target_id = AccountId::try_from(ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET)?;
let attachment = NetworkAccountTarget::new(target_id, NoteExecutionHint::Always)?;
let note = NoteBuilder::new(sender.id(), &mut rng)
.note_type(NoteType::Public)
.attachment(attachment)
.build()?;
assert!(note.is_network_note());
let expected_note_type = note.metadata().note_type();
let network_note = note.into_account_target_network_note()?;
assert_eq!(network_note.target_account_id(), target_id);
assert_eq!(network_note.execution_hint(), NoteExecutionHint::Always);
assert_eq!(network_note.note_type(), expected_note_type);
let valid_note = NoteBuilder::new(sender.id(), &mut rng)
.note_type(NoteType::Public)
.attachment(attachment)
.build()?;
let try_from_note = AccountTargetNetworkNote::try_from(valid_note)?;
assert_eq!(try_from_note.target_account_id(), target_id);
let non_network_note =
NoteBuilder::new(sender.id(), &mut rng).note_type(NoteType::Public).build()?;
assert!(!non_network_note.is_network_note());
assert!(AccountTargetNetworkNote::new(non_network_note.clone()).is_err());
assert!(non_network_note.clone().into_account_target_network_note().is_err());
assert!(AccountTargetNetworkNote::try_from(non_network_note).is_err());
let private_network_note = NoteBuilder::new(sender.id(), &mut rng)
.note_type(NoteType::Private)
.attachment(attachment)
.build()?;
assert!(!private_network_note.is_network_note());
assert!(AccountTargetNetworkNote::new(private_network_note.clone()).is_err());
assert!(private_network_note.clone().into_account_target_network_note().is_err());
assert!(AccountTargetNetworkNote::try_from(private_network_note).is_err());
Ok(())
}
fn create_output_note(note: &Note) -> String {
let mut create_note_code = format!(
"
# create an output note
push.{RECIPIENT}
push.{note_type}
push.{tag}
exec.output_note::create
# => [note_idx]
",
RECIPIENT = note.recipient().digest(),
note_type = note.metadata().note_type() as u8,
tag = Felt::from(note.metadata().tag()),
);
for asset in note.assets().iter() {
create_note_code.push_str(&format!(
"
# move the asset to the note
dup
push.{ASSET_VALUE}
push.{ASSET_KEY}
# => [ASSET_KEY, ASSET_VALUE, note_idx, note_idx]
call.::miden::standards::wallets::basic::move_asset_to_note
# => [note_idx]
",
ASSET_KEY = asset.to_key_word(),
ASSET_VALUE = asset.to_value_word()
));
}
create_note_code
}