use alloc::vec::Vec;
use std::collections::BTreeMap;
use std::string::String;
use std::sync::LazyLock;
use anyhow::Context;
use assert_matches::assert_matches;
use miden_crypto::rand::test_utils::rand_value;
use miden_protocol::account::{
Account,
AccountBuilder,
AccountCode,
AccountComponent,
AccountComponentCode,
AccountComponentMetadata,
AccountDelta,
AccountId,
AccountPatch,
AccountStorage,
AccountStoragePatch,
AccountType,
AccountVaultDelta,
AccountVaultPatch,
StorageMap,
StorageMapKey,
StorageMapPatch,
StorageSlot,
StorageSlotName,
StorageSlotPatch,
StorageValuePatch,
};
use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
use miden_protocol::note::{NoteTag, NoteType};
use miden_protocol::testing::account_id::AccountIdBuilder;
use miden_protocol::testing::storage::{MOCK_MAP_SLOT, MOCK_VALUE_SLOT0};
use miden_protocol::transaction::TransactionScript;
use miden_protocol::{EMPTY_WORD, Felt, Word, ZERO};
use miden_standards::code_builder::CodeBuilder;
use miden_standards::testing::account_component::MockAccountComponent;
use miden_tx::{LocalTransactionProver, TransactionExecutorError};
use rand::RngExt;
use crate::{Auth, MockChain, TestTransactionBuilder};
#[tokio::test]
async fn empty_account_delta_commitment_is_empty_word() -> anyhow::Result<()> {
let tx_script = CodeBuilder::with_mock_libraries()
.compile_tx_script(
r#"
use miden::core::sys
use mock::account as mock_account
@transaction_script
pub proc main
call.mock_account::compute_delta_commitment
# => [DELTA_COMMITMENT, pad(12)]
padw assert_eqw.err="empty account delta should commit to the empty word"
exec.sys::truncate_stack
end
"#,
)
.context("failed to compile tx script")?;
let mut builder = MockChain::builder();
let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
let mock_chain = builder.build()?;
mock_chain
.build_tx_context(account.id(), &[], &[])
.expect("failed to build tx context")
.tx_script(tx_script)
.build()?
.execute()
.await
.context("failed to execute transaction")?;
Ok(())
}
#[tokio::test]
async fn delta_nonce() -> anyhow::Result<()> {
AccountUpdateTest {
initial_storage_slots: vec![],
initial_vault_assets: vec![],
input_notes_assets: vec![],
tx_script: None,
expected_storage_patch: AccountStoragePatch::new(),
expected_vault_delta: AccountVaultDelta::default(),
expected_vault_patch: AccountVaultPatch::default(),
expected_code: None,
}
.execute()
.await
}
#[tokio::test]
async fn storage_patch_for_value_slots() -> anyhow::Result<()> {
let slot_0_name = StorageSlotName::mock(0);
let slot_0_init_value = Word::from([2, 4, 6, 8u32]);
let slot_0_tmp_value = Word::from([3, 4, 5, 6u32]);
let slot_0_final_value = EMPTY_WORD;
let slot_1_name = StorageSlotName::mock(1);
let slot_1_init_value = EMPTY_WORD;
let slot_1_final_value = Word::from([3, 4, 5, 6u32]);
let slot_2_name = StorageSlotName::mock(2);
let slot_2_init_value = Word::from([1, 3, 5, 7u32]);
let slot_2_final_value = slot_2_init_value;
let slot_3_name = StorageSlotName::mock(3);
let slot_3_init_value = Word::from([1, 3, 5, 7u32]);
let slot_3_tmp_value = Word::from([2, 3, 4, 5u32]);
let slot_3_final_value = slot_3_init_value;
let tx_script = parse_tx_script(format!(
r#"
const SLOT_0_NAME = word("{slot_0_name}")
const SLOT_1_NAME = word("{slot_1_name}")
const SLOT_2_NAME = word("{slot_2_name}")
const SLOT_3_NAME = word("{slot_3_name}")
@transaction_script
pub proc main
push.{slot_0_tmp_value}
push.SLOT_0_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, VALUE]
exec.set_item
# => []
push.{slot_0_final_value}
push.SLOT_0_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, VALUE]
exec.set_item
# => []
push.{slot_1_final_value}
push.SLOT_1_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, VALUE]
exec.set_item
# => []
push.{slot_2_final_value}
push.SLOT_2_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, VALUE]
exec.set_item
# => []
push.{slot_3_tmp_value}
push.SLOT_3_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, VALUE]
exec.set_item
# => []
push.{slot_3_final_value}
push.SLOT_3_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, VALUE]
exec.set_item
# => []
end
"#
))?;
let expected_storage_patch = AccountStoragePatch::builder()
.update_value(slot_0_name.clone(), slot_0_final_value)
.update_value(slot_1_name.clone(), slot_1_final_value)
.build();
AccountUpdateTest {
initial_storage_slots: vec![
StorageSlot::with_value(slot_0_name, slot_0_init_value),
StorageSlot::with_value(slot_1_name, slot_1_init_value),
StorageSlot::with_value(slot_2_name, slot_2_init_value),
StorageSlot::with_value(slot_3_name, slot_3_init_value),
],
initial_vault_assets: vec![],
input_notes_assets: vec![],
tx_script: Some(tx_script),
expected_storage_patch,
expected_vault_delta: AccountVaultDelta::default(),
expected_vault_patch: AccountVaultPatch::default(),
expected_code: None,
}
.execute()
.await
}
#[tokio::test]
async fn storage_patch_for_map_slots() -> anyhow::Result<()> {
let key0 = StorageMapKey::from_raw(rand_value::<Word>());
let key1 = StorageMapKey::from_raw(rand_value::<Word>());
let key2 = StorageMapKey::from_raw(rand_value::<Word>());
let key3 = StorageMapKey::from_raw(rand_value::<Word>());
let key4 = StorageMapKey::from_raw(rand_value::<Word>());
let key5 = StorageMapKey::from_raw(rand_value::<Word>());
let key0_init_value = EMPTY_WORD;
let key1_init_value = EMPTY_WORD;
let key2_init_value = Word::from([1, 2, 3, 4u32]);
let key3_init_value = Word::from([1, 2, 3, 4u32]);
let key4_init_value = Word::from([1, 2, 3, 4u32]);
let key5_init_value = Word::from([1, 2, 3, 4u32]);
let key0_final_value = Word::from([1, 2, 3, 4u32]);
let key1_tmp_value = Word::from([1, 2, 3, 4u32]);
let key1_final_value = Word::from([2, 3, 4, 5u32]);
let key2_final_value = key2_init_value;
let key3_final_value = EMPTY_WORD;
let key4_tmp_value = Word::from([2, 3, 4, 5u32]);
let key4_final_value = Word::from([1, 2, 3, 4u32]);
let key5_tmp_value = Word::from([2, 3, 4, 5u32]);
let key5_final_value = Word::from([1, 2, 3, 4u32]);
let slot_0_name = StorageSlotName::mock(0);
let mut map0 = StorageMap::new();
map0.insert(key0, key0_init_value).unwrap();
map0.insert(key1, key1_init_value).unwrap();
let slot_1_name = StorageSlotName::mock(1);
let mut map1 = StorageMap::new();
map1.insert(key2, key2_init_value).unwrap();
map1.insert(key3, key3_init_value).unwrap();
map1.insert(key4, key4_init_value).unwrap();
let slot_2_name = StorageSlotName::mock(2);
let mut map2 = StorageMap::new();
map2.insert(key5, key5_init_value).unwrap();
let tx_script = parse_tx_script(format!(
r#"
const SLOT_0_NAME = word("{slot_0_name}")
const SLOT_1_NAME = word("{slot_1_name}")
const SLOT_2_NAME = word("{slot_2_name}")
@transaction_script
pub proc main
push.{key0_final_value} push.{key0}
push.SLOT_0_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, KEY, VALUE]
exec.set_map_item
# => []
push.{key1_tmp_value} push.{key1}
push.SLOT_0_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, KEY, VALUE]
exec.set_map_item
# => []
push.{key1_final_value} push.{key1}
push.SLOT_0_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, KEY, VALUE]
exec.set_map_item
# => []
push.{key2_final_value} push.{key2}
push.SLOT_1_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, KEY, VALUE]
exec.set_map_item
# => []
push.{key3_final_value} push.{key3}
push.SLOT_1_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, KEY, VALUE]
exec.set_map_item
# => []
push.{key4_tmp_value} push.{key4}
push.SLOT_1_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, KEY, VALUE]
exec.set_map_item
# => []
push.{key4_final_value} push.{key4}
push.SLOT_1_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, KEY, VALUE]
exec.set_map_item
# => []
push.{key5_tmp_value} push.{key5}
push.SLOT_2_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, KEY, VALUE]
exec.set_map_item
# => []
push.{key5_final_value} push.{key5}
push.SLOT_2_NAME[0..2]
# => [slot_id_suffix, slot_id_prefix, KEY, VALUE]
exec.set_map_item
# => []
end
"#
))?;
let expected_storage_patch = AccountStoragePatch::builder()
.update_map(slot_0_name.clone(), [(key0, key0_final_value), (key1, key1_final_value)])
.update_map(slot_1_name.clone(), [(key3, key3_final_value)])
.build();
AccountUpdateTest {
initial_storage_slots: vec![
StorageSlot::with_map(slot_0_name, map0),
StorageSlot::with_map(slot_1_name, map1),
StorageSlot::with_map(slot_2_name, map2),
StorageSlot::with_map(StorageSlotName::mock(3), StorageMap::new()),
],
initial_vault_assets: vec![],
input_notes_assets: vec![],
tx_script: Some(tx_script),
expected_storage_patch,
expected_vault_delta: AccountVaultDelta::default(),
expected_vault_patch: AccountVaultPatch::default(),
expected_code: None,
}
.execute()
.await
}
#[tokio::test]
async fn fungible_asset_update() -> anyhow::Result<()> {
let faucet0: AccountId = AccountIdBuilder::new()
.account_type(AccountType::Private)
.build_with_seed(rand::random());
let faucet1: AccountId = AccountIdBuilder::new()
.account_type(AccountType::Public)
.build_with_seed(rand::random());
let faucet2: AccountId = AccountIdBuilder::new().build_with_seed(rand::random());
let faucet3: AccountId = AccountIdBuilder::new().build_with_seed(rand::random());
let faucet4: AccountId = AccountIdBuilder::new().build_with_seed(rand::random());
let max_amount = FungibleAsset::MAX_AMOUNT.as_u64();
let original_asset0 = FungibleAsset::new(faucet0, 300)?;
let original_asset1 = FungibleAsset::new(faucet1, 200)?;
let original_asset2 = FungibleAsset::new(faucet2, 100)?;
let original_asset3 = FungibleAsset::new(faucet3, max_amount)?;
let added_asset0 = FungibleAsset::new(faucet0, 100)?;
let added_asset1 = FungibleAsset::new(faucet1, 100)?;
let added_asset2 = FungibleAsset::new(faucet2, 200)?;
let added_asset4 = FungibleAsset::new(faucet4, max_amount)?;
let removed_asset0 = FungibleAsset::new(faucet0, 200)?;
let removed_asset1 = FungibleAsset::new(faucet1, 100)?;
let removed_asset2 = FungibleAsset::new(faucet2, 100)?;
let removed_asset3 = FungibleAsset::new(faucet3, max_amount)?;
let tx_script = parse_tx_script(format!(
"
@transaction_script
pub proc main
push.{ASSET0_VALUE} push.{ASSET0_KEY}
exec.util::create_default_note_with_moved_asset
# => []
push.{ASSET1_VALUE} push.{ASSET1_KEY}
exec.util::create_default_note_with_moved_asset
# => []
push.{ASSET2_VALUE} push.{ASSET2_KEY}
exec.util::create_default_note_with_moved_asset
# => []
push.{ASSET3_VALUE} push.{ASSET3_KEY}
exec.util::create_default_note_with_moved_asset
# => []
end
",
ASSET0_KEY = removed_asset0.to_id_word(),
ASSET0_VALUE = removed_asset0.to_value_word(),
ASSET1_KEY = removed_asset1.to_id_word(),
ASSET1_VALUE = removed_asset1.to_value_word(),
ASSET2_KEY = removed_asset2.to_id_word(),
ASSET2_VALUE = removed_asset2.to_value_word(),
ASSET3_KEY = removed_asset3.to_id_word(),
ASSET3_VALUE = removed_asset3.to_value_word(),
))?;
let expected_vault_delta = AccountVaultDelta::from_iters(
[Asset::from(added_asset2.sub(removed_asset2)?), Asset::from(added_asset4)],
[Asset::from(removed_asset0.sub(added_asset0)?), Asset::from(removed_asset3)],
);
let mut expected_vault_patch = AccountVaultPatch::default();
expected_vault_patch
.insert_asset(original_asset0.add(added_asset0)?.sub(removed_asset0)?.into());
expected_vault_patch
.insert_asset(original_asset2.add(added_asset2)?.sub(removed_asset2)?.into());
expected_vault_patch.remove_asset(removed_asset3.id());
expected_vault_patch.insert_asset(added_asset4.into());
AccountUpdateTest {
initial_storage_slots: vec![],
initial_vault_assets: vec![
original_asset0.into(),
original_asset1.into(),
original_asset2.into(),
original_asset3.into(),
],
input_notes_assets: vec![
added_asset0.into(),
added_asset1.into(),
added_asset2.into(),
added_asset4.into(),
],
tx_script: Some(tx_script),
expected_storage_patch: AccountStoragePatch::new(),
expected_vault_delta,
expected_vault_patch,
expected_code: None,
}
.execute()
.await
}
#[tokio::test]
async fn non_fungible_asset_delta() -> anyhow::Result<()> {
let mut rng = rand::rng();
let faucet0: AccountId = AccountIdBuilder::new().build_with_seed(rng.random());
let faucet1: AccountId = AccountIdBuilder::new().build_with_seed(rng.random());
let faucet2: AccountId = AccountIdBuilder::new()
.account_type(AccountType::Public)
.build_with_seed(rng.random());
let faucet3: AccountId = AccountIdBuilder::new()
.account_type(AccountType::Private)
.build_with_seed(rng.random());
let asset0 = NonFungibleAsset::new(&NonFungibleAssetDetails::new(
faucet0,
rng.random::<[u8; 32]>().to_vec(),
));
let asset1 = NonFungibleAsset::new(&NonFungibleAssetDetails::new(
faucet1,
rng.random::<[u8; 32]>().to_vec(),
));
let asset2 = NonFungibleAsset::new(&NonFungibleAssetDetails::new(
faucet2,
rng.random::<[u8; 32]>().to_vec(),
));
let asset3 = NonFungibleAsset::new(&NonFungibleAssetDetails::new(
faucet3,
rng.random::<[u8; 32]>().to_vec(),
));
let tx_script = parse_tx_script(format!(
"
@transaction_script
pub proc main
push.{ASSET1_VALUE} push.{ASSET1_KEY}
exec.util::create_default_note_with_moved_asset
# => []
push.{ASSET2_VALUE} push.{ASSET2_KEY}
exec.util::create_default_note_with_moved_asset
# => []
# remove asset 3
push.{ASSET3_VALUE}
push.{ASSET3_KEY}
exec.remove_asset
# => [FINAL_ASSET_VALUE]
dropw
# re-add asset 3
push.{ASSET3_VALUE}
push.{ASSET3_KEY}
# => [ASSET_ID, ASSET_VALUE]
exec.add_asset dropw
# => []
end
",
ASSET1_KEY = asset1.to_id_word(),
ASSET1_VALUE = asset1.to_value_word(),
ASSET2_KEY = asset2.to_id_word(),
ASSET2_VALUE = asset2.to_value_word(),
ASSET3_KEY = asset3.to_id_word(),
ASSET3_VALUE = asset3.to_value_word(),
))?;
let expected_vault_delta =
AccountVaultDelta::from_iters([Asset::from(asset0)], [Asset::from(asset1)]);
let mut expected_vault_patch = AccountVaultPatch::default();
expected_vault_patch.insert_asset(asset0.into());
expected_vault_patch.remove_asset(Asset::from(asset1).id());
AccountUpdateTest {
initial_storage_slots: vec![],
initial_vault_assets: vec![asset1.into(), asset3.into()],
input_notes_assets: vec![asset0.into(), asset2.into()],
tx_script: Some(tx_script),
expected_storage_patch: AccountStoragePatch::new(),
expected_vault_delta,
expected_vault_patch,
expected_code: None,
}
.execute()
.await
}
#[tokio::test]
async fn asset_and_storage_patch() -> anyhow::Result<()> {
let mut rng = rand::rng();
let faucet_0: AccountId = AccountIdBuilder::new().build_with_seed(rng.random());
let faucet_1: AccountId = AccountIdBuilder::new().build_with_seed(rng.random());
let faucet_2: AccountId = AccountIdBuilder::new().build_with_seed(rng.random());
let faucet_3: AccountId = AccountIdBuilder::new().build_with_seed(rng.random());
let asset_0: Asset = FungibleAsset::new(faucet_0, 1000)?.into();
let asset_1: Asset = NonFungibleAsset::new(&NonFungibleAssetDetails::new(
faucet_1,
rng.random::<[u8; 32]>().to_vec(),
))
.into();
let asset_2: Asset = FungibleAsset::new(faucet_2, 500)?.into();
let asset_3: Asset = NonFungibleAsset::new(&NonFungibleAssetDetails::new(
faucet_3,
rng.random::<[u8; 32]>().to_vec(),
))
.into();
let updated_slot_value = Word::from([7, 9, 11, 13u32]);
let updated_map_key = StorageMapKey::from_array([14, 15, 16, 17u32]);
let updated_map_value = Word::from([18, 19, 20, 21u32]);
let removed_assets = [asset_0, asset_1];
let added_assets = [asset_2, asset_3];
let mut send_assets_script = String::new();
for (i, removed_asset) in removed_assets.iter().enumerate() {
send_assets_script.push_str(&format!(
"
### note {i}
# prepare the stack for a new note creation
push.0.1.2.3 # RECIPIENT
push.{note_type} # note_type
push.{tag} # tag
# create the note
call.::mock::account::create_note
# => [note_idx, pad(15)]
# move the asset into the new note
swapw dropw
push.{ASSET_VALUE} push.{ASSET_ID}
call.::miden::standards::wallets::basic::move_asset_to_note
# => [pad(16)]
# clear the stack
dropw dropw dropw dropw
",
note_type = NoteType::Private as u8,
tag = NoteTag::default(),
ASSET_ID = removed_asset.to_id_word(),
ASSET_VALUE = removed_asset.to_value_word(),
));
}
let tx_script_src = format!(
r#"
use mock::account
use miden::protocol::output_note
const MOCK_VALUE_SLOT0 = word("{mock_value_slot0}")
const MOCK_MAP_SLOT = word("{mock_map_slot}")
@transaction_script
pub proc main
## Update value storage slot
push.{updated_slot_value}
push.MOCK_VALUE_SLOT0[0..2]
call.account::set_item dropw
## Update map storage slot at a previously-unset key
push.{updated_map_value}
push.{updated_map_key}
push.MOCK_MAP_SLOT[0..2]
call.account::set_map_item dropw dropw dropw
## Move both initial vault assets out via newly created output notes
{send_assets_script}
dropw dropw dropw dropw
end
"#,
mock_value_slot0 = &*MOCK_VALUE_SLOT0,
mock_map_slot = &*MOCK_MAP_SLOT,
);
let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(tx_script_src)?;
let expected_storage_patch = AccountStoragePatch::builder()
.update_value(MOCK_VALUE_SLOT0.clone(), updated_slot_value)
.update_map(MOCK_MAP_SLOT.clone(), [(updated_map_key, updated_map_value)])
.build();
let expected_vault_delta = AccountVaultDelta::from_iters(added_assets, removed_assets);
let mut expected_vault_patch = AccountVaultPatch::default();
expected_vault_patch.remove_asset(asset_0.id());
expected_vault_patch.remove_asset(asset_1.id());
expected_vault_patch.insert_asset(asset_2);
expected_vault_patch.insert_asset(asset_3);
AccountUpdateTest {
initial_storage_slots: AccountStorage::mock_storage_slots(),
initial_vault_assets: vec![asset_0, asset_1],
input_notes_assets: vec![asset_2, asset_3],
tx_script: Some(tx_script),
expected_storage_patch,
expected_vault_delta,
expected_vault_patch,
expected_code: None,
}
.execute()
.await
}
#[tokio::test]
async fn proven_tx_storage_maps_matches_executed_tx_for_new_account() -> anyhow::Result<()> {
let map0 = StorageMap::with_entries([(StorageMapKey::from_raw(rand_value()), rand_value())])?;
let map1 = map0.clone();
let mut map2 = StorageMap::with_entries([
(StorageMapKey::from_raw(rand_value()), rand_value()),
(StorageMapKey::from_raw(rand_value()), rand_value()),
(StorageMapKey::from_raw(rand_value()), rand_value()),
(StorageMapKey::from_raw(rand_value()), rand_value()),
])?;
let map0_slot_name = StorageSlotName::mock(1);
let map1_slot_name = StorageSlotName::mock(2);
let map2_slot_name = StorageSlotName::mock(4);
let account = AccountBuilder::new([1; 32])
.account_type(AccountType::Public)
.with_auth_component(delta_check_auth_component())
.with_component(MockAccountComponent::with_slots(vec![
AccountStorage::mock_value_slot0(),
StorageSlot::with_map(map0_slot_name.clone(), map0.clone()),
StorageSlot::with_map(map1_slot_name.clone(), map1.clone()),
AccountStorage::mock_value_slot1(),
StorageSlot::with_map(map2_slot_name.clone(), map2.clone()),
]))
.build()?;
let existing_key = *map2.entries().next().unwrap().0;
let value0 = Word::from([3, 4, 5, 6u32]);
let code = format!(
r#"
use mock::account
const MAP_SLOT=word("{map2_slot_name}")
@transaction_script
pub proc main
# Update an existing key.
push.{value0}
push.{existing_key}
push.MAP_SLOT[0..2]
# => [slot_id_suffix, slot_id_prefix, KEY, VALUE]
call.account::set_map_item
exec.::miden::core::sys::truncate_stack
end
"#
);
let builder = CodeBuilder::with_mock_libraries();
let source_manager = builder.source_manager();
let tx_script = builder.compile_tx_script(code)?;
let tx_builder = TestTransactionBuilder::new(account.clone())
.tx_script(tx_script)
.with_source_manager(source_manager);
let tx_summary = tx_builder
.clone()
.auth_args(emit_delta_args())
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let tx = tx_builder.build()?.execute().await?;
map2.insert(existing_key, value0)?;
for (slot_name, expected_map) in
[(map0_slot_name, map0), (map1_slot_name, map1), (map2_slot_name, map2)]
{
let map_patch_entries = tx
.account_patch()
.storage()
.created_map(&slot_name)
.expect("created map patch should be present")
.as_map();
let expected: BTreeMap<_, _> = expected_map.entries().map(|(k, v)| (*k, *v)).collect();
assert_eq!(map_patch_entries, &expected, "map delta does not match for slot {slot_name}",);
}
let proven_tx = LocalTransactionProver::default().prove_dummy(tx.clone())?;
let proven_tx_patch = proven_tx.account_update().details().unwrap_public();
let proven_tx_account = Account::try_from(proven_tx_patch)?;
let exec_tx_account = Account::try_from(tx.account_patch())?;
let exec_tx_delta_account = Account::try_from(tx_summary.account_delta())?;
assert_eq!(exec_tx_delta_account, exec_tx_account);
assert_eq!(proven_tx_account.storage(), exec_tx_account.storage());
let proven_tx_patch_converted = AccountPatch::try_from(proven_tx_account.clone())?;
let exec_tx_patch_converted = AccountPatch::try_from(exec_tx_account.clone())?;
let proven_tx_delta_converted = AccountDelta::try_from(proven_tx_account)?;
let exec_tx_delta_converted = AccountDelta::try_from(exec_tx_account)?;
assert_eq!(proven_tx_delta_converted, exec_tx_delta_converted);
assert_eq!(tx.account_patch(), proven_tx_patch);
assert_eq!(&proven_tx_patch_converted, tx.account_patch());
assert_eq!(&exec_tx_patch_converted, tx.account_patch());
assert_eq!(&exec_tx_delta_converted, tx_summary.account_delta());
assert_eq!(&proven_tx_delta_converted, tx_summary.account_delta());
assert_eq!(exec_tx_patch_converted.to_commitment(), tx.account_patch().to_commitment());
assert_eq!(proven_tx_patch_converted.to_commitment(), tx.account_patch().to_commitment());
assert_eq!(
exec_tx_delta_converted.to_commitment(),
tx_summary.account_delta().to_commitment()
);
assert_eq!(
proven_tx_delta_converted.to_commitment(),
tx_summary.account_delta().to_commitment()
);
Ok(())
}
#[tokio::test]
async fn patch_for_new_account_retains_empty_value_storage_slots() -> anyhow::Result<()> {
let slot_name0 = StorageSlotName::mock(0);
let slot_name1 = StorageSlotName::mock(1);
let slot_value2 = Word::from([1, 2, 3, 4u32]);
let mut account = AccountBuilder::new(rand::random())
.account_type(AccountType::Public)
.with_component(MockAccountComponent::with_slots(vec![
StorageSlot::with_empty_value(slot_name0.clone()),
StorageSlot::with_value(slot_name1.clone(), slot_value2),
]))
.with_auth_component(Auth::IncrNonce)
.build()?;
let tx = TestTransactionBuilder::new(account.clone()).build()?.execute().await?;
let proven_tx = LocalTransactionProver::default().prove_dummy(tx.clone())?;
let patch = proven_tx.account_update().details().unwrap_public();
assert_eq!(patch.storage().values().count(), 2);
assert_matches!(
patch.storage().get(&slot_name0).unwrap(),
StorageSlotPatch::Value(StorageValuePatch::Create { value }) => {
assert_eq!(*value, Word::empty())
}
);
assert_matches!(
patch.storage().get(&slot_name1).unwrap(),
StorageSlotPatch::Value(StorageValuePatch::Create { value }) => {
assert_eq!(*value, slot_value2)
}
);
let recreated_account = Account::try_from(patch)?;
account.increment_nonce(Felt::ONE)?;
assert_eq!(recreated_account, account);
Ok(())
}
#[tokio::test]
async fn patch_for_new_account_retains_empty_map_storage_slots() -> anyhow::Result<()> {
let slot_name0 = StorageSlotName::mock(0);
let slot_name1 = StorageSlotName::mock(1);
let mut account = AccountBuilder::new(rand::random())
.account_type(AccountType::Public)
.with_component(MockAccountComponent::with_slots(vec![
StorageSlot::with_empty_map(slot_name0.clone()),
StorageSlot::with_empty_map(slot_name1.clone()),
]))
.with_auth_component(Auth::IncrNonce)
.build()?;
let map_key = StorageMapKey::from_array([1, 2, 3, 4u32]);
let non_empty_value = Word::from([5, 6, 7, 8u32]);
let code = format!(
r#"
use mock::account
const MAP_SLOT=word("{slot_name1}")
@transaction_script
pub proc main
# Set the key to a non-empty value.
push.{non_empty_value}
push.{map_key}
push.MAP_SLOT[0..2]
# => [slot_id_suffix, slot_id_prefix, KEY, VALUE]
call.account::set_map_item
# => [OLD_VALUE, pad(12)]
dropw
# Set the same key back to an empty value, which should be normalized away.
padw
push.{map_key}
push.MAP_SLOT[0..2]
# => [slot_id_suffix, slot_id_prefix, KEY, EMPTY_VALUE]
call.account::set_map_item
exec.::miden::core::sys::truncate_stack
end
"#
);
let builder = CodeBuilder::with_mock_libraries();
let source_manager = builder.source_manager();
let tx_script = builder.compile_tx_script(code)?;
let tx = TestTransactionBuilder::new(account.clone())
.tx_script(tx_script)
.with_source_manager(source_manager)
.build()?
.execute()
.await?;
let proven_tx = LocalTransactionProver::default().prove_dummy(tx.clone())?;
let patch = proven_tx.account_update().details().unwrap_public();
assert_eq!(patch.storage().maps().count(), 2);
for slot_name in [&slot_name0, &slot_name1] {
assert_matches!(
patch.storage().get(slot_name).unwrap(),
StorageSlotPatch::Map(StorageMapPatch::Create { entries }) => {
assert!(entries.is_empty())
}
);
}
let recreated_account = Account::try_from(patch)?;
account.increment_nonce(Felt::ONE)?;
assert_eq!(recreated_account, account);
Ok(())
}
#[tokio::test]
async fn adding_amount_zero_fungible_asset_to_account_vault_works() -> anyhow::Result<()> {
AccountUpdateTest {
initial_storage_slots: vec![],
initial_vault_assets: vec![],
input_notes_assets: vec![FungibleAsset::mock(0)],
tx_script: None,
expected_storage_patch: AccountStoragePatch::new(),
expected_vault_delta: AccountVaultDelta::default(),
expected_vault_patch: AccountVaultPatch::default(),
expected_code: None,
}
.execute()
.await
}
#[tokio::test]
async fn recomputing_delta_resets_host_delta() -> anyhow::Result<()> {
let mut rng = rand::rng();
let faucet0: AccountId = AccountIdBuilder::new().build_with_seed(rng.random());
let faucet1: AccountId = AccountIdBuilder::new().build_with_seed(rng.random());
let asset0 = NonFungibleAsset::new(&NonFungibleAssetDetails::new(
faucet0,
rng.random::<[u8; 32]>().to_vec(),
));
let asset1 = NonFungibleAsset::new(&NonFungibleAssetDetails::new(
faucet1,
rng.random::<[u8; 32]>().to_vec(),
));
let auth_code = format!(
"
use miden::protocol::native_account
use {{AUTH_UNAUTHORIZED_EVENT}} from miden::protocol::auth
{TEST_ACCOUNT_CONVENIENCE_WRAPPERS}
#! Inputs: [[AUTH_ARGS], pad(12)]
#! Outputs: [pad(16)]
@auth_script
pub proc auth_test
exec.native_account::incr_nonce drop
# => [[AUTH_ARGS], pad(12)]
# add asset 0 to the vault
push.{ASSET0_VALUE} push.{ASSET0_KEY}
exec.add_asset dropw
# => [[AUTH_ARGS], pad(12)]
# compute the delta to trigger the host to add the assets to the vault delta
exec.native_account::compute_delta_commitment
dropw
# => [[AUTH_ARGS], pad(12)]
# remove asset 0 for correct asset preservation
push.{ASSET0_VALUE}
push.{ASSET0_KEY}
exec.remove_asset dropw
# => [[AUTH_ARGS], pad(12)]
# Build the tx summary.
# Replace AUTH_ARGS with an EMPTY_WORD salt for the tx summary.
dropw padw
# => [SALT, pad(12)]
exec.::miden::standards::auth::create_tx_summary
# => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT, pad(12)]
adv.insert_hqword
# => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT, pad(12)]
exec.::miden::standards::auth::hash_tx_summary
# => [TX_SUMMARY_COMMITMENT, pad(12)]
emit.AUTH_UNAUTHORIZED_EVENT
push.0 assert.err=\"emitting the event should have aborted execution\"
end
",
ASSET0_KEY = asset0.to_id_word(),
ASSET0_VALUE = asset0.to_value_word(),
);
let auth_component_code =
CodeBuilder::with_mock_libraries().compile_component_code("test::account", auth_code)?;
let mut builder = MockChain::builder();
let account = Account::builder(builder.rng_mut().random())
.account_type(AccountType::Public)
.with_auth_component(AccountComponent::new(
auth_component_code,
vec![],
AccountComponentMetadata::new("test::account"),
)?)
.with_component(MockAccountComponent::with_slots(vec![]))
.with_assets(vec![asset1.into()])
.build_existing()?;
builder.add_account(account.clone())?;
let mock_chain = builder.build()?;
let tx_summary = mock_chain
.build_tx_context(account, &[], &[])?
.build()?
.execute()
.await
.unwrap_err()
.unwrap_unauthorized_err();
let account_delta = tx_summary.account_delta();
assert!(account_delta.vault().is_empty(), "vault delta should be effectively empty");
assert!(account_delta.storage().is_empty(), "storage delta should be empty");
assert_eq!(
account_delta.nonce_delta().as_canonical_u64(),
1,
"nonce should have been incremented"
);
Ok(())
}
fn parse_tx_script(code: impl AsRef<str>) -> anyhow::Result<TransactionScript> {
let code = format!(
"
{TEST_ACCOUNT_CONVENIENCE_WRAPPERS}
{code}
",
code = code.as_ref()
);
CodeBuilder::with_mock_libraries()
.compile_tx_script(&code)
.context("failed to parse tx script")
}
const TEST_ACCOUNT_CONVENIENCE_WRAPPERS: &str = "
use mock::account
use mock::util
use miden::protocol::output_note
#! Inputs: [slot_id_suffix, slot_id_prefix, VALUE]
#! Outputs: []
proc set_item
repeat.10 push.0 movdn.6 end
# => [slot_id_suffix, slot_id_prefix, VALUE, pad(10)]
call.account::set_item
# => [OLD_VALUE, pad(12)]
dropw dropw dropw dropw
end
#! Inputs: [slot_id_suffix, slot_id_prefix, KEY, VALUE]
#! Outputs: []
proc set_map_item
repeat.6 push.0 movdn.10 end
# => [index, KEY, VALUE, pad(6)]
call.account::set_map_item
# => [OLD_VALUE, pad(12)]
dropw dropw dropw dropw
# => []
end
#! Inputs: [ASSET_ID, ASSET_VALUE]
#! Outputs: [FINAL_ASSET_VALUE]
proc add_asset
repeat.8 push.0 movdn.8 end
# => [ASSET_ID, ASSET_VALUE, pad(8)]
call.account::add_asset
# => [FINAL_ASSET_VALUE, pad(12)]
repeat.12 movup.4 drop end
# => [FINAL_ASSET_VALUE]
end
#! Inputs: [ASSET_ID, ASSET_VALUE]
#! Outputs: [ASSET_VALUE]
proc remove_asset
padw padw swapdw
# => [ASSET_ID, ASSET_VALUE, pad(8)]
call.account::remove_asset
# => [ASSET_VALUE, pad(12)]
repeat.12 movup.4 drop end
# => [ASSET_VALUE]
end
";
const DELTA_CHECK_AUTH_CODE: &str = r#"
use miden::protocol::native_account
use {AUTH_UNAUTHORIZED_EVENT} from miden::protocol::auth
#! Inputs: [[should_emit, 0, 0, 0], pad(12)]
#! Outputs: [pad(16)]
@auth_script
pub proc auth_incr_nonce_with_delta_check
exec.native_account::incr_nonce drop
# => [[should_emit, 0, 0, 0], pad(12)]
dup
if.true
# Replace AUTH_ARGS with an EMPTY_WORD salt for the tx summary.
dropw padw
# => [SALT, pad(12)]
exec.::miden::standards::auth::create_tx_summary
# => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT, pad(12)]
adv.insert_hqword
# => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT, pad(12)]
exec.::miden::standards::auth::hash_tx_summary
# => [TX_SUMMARY_COMMITMENT, pad(12)]
emit.AUTH_UNAUTHORIZED_EVENT
push.0 assert.err="emitting the event should have aborted execution"
end
end
"#;
static DELTA_CHECK_AUTH_LIBRARY: LazyLock<AccountComponentCode> = LazyLock::new(|| {
CodeBuilder::with_mock_libraries()
.compile_component_code("test::incr_nonce_with_delta_check_auth", DELTA_CHECK_AUTH_CODE)
.expect("delta-check auth code should compile")
});
fn delta_check_auth_component() -> AccountComponent {
AccountComponent::new(
DELTA_CHECK_AUTH_LIBRARY.clone(),
vec![],
AccountComponentMetadata::new("test::incr_nonce_with_delta_check_auth"),
)
.expect("delta-check auth component should be valid")
}
struct AccountUpdateTest {
pub initial_storage_slots: Vec<StorageSlot>,
pub initial_vault_assets: Vec<Asset>,
pub input_notes_assets: Vec<Asset>,
pub tx_script: Option<TransactionScript>,
pub expected_storage_patch: AccountStoragePatch,
pub expected_vault_delta: AccountVaultDelta,
pub expected_vault_patch: AccountVaultPatch,
pub expected_code: Option<AccountCode>,
}
impl AccountUpdateTest {
async fn execute(self) -> anyhow::Result<()> {
let Self {
initial_storage_slots,
initial_vault_assets,
input_notes_assets,
tx_script,
expected_storage_patch,
expected_vault_delta,
expected_vault_patch,
expected_code,
} = self;
let mut builder = MockChain::builder();
let account = Account::builder(builder.rng_mut().random())
.account_type(AccountType::Public)
.with_auth_component(delta_check_auth_component())
.with_component(MockAccountComponent::with_slots(initial_storage_slots))
.with_assets(initial_vault_assets)
.build_existing()?;
builder.add_account(account.clone())?;
let mut input_note_ids = Vec::with_capacity(input_notes_assets.len());
for note_asset in input_notes_assets {
let input_note = builder
.add_p2id_note(account.id(), account.id(), &[note_asset], NoteType::Public)
.context("failed to add note with assets")?;
input_note_ids.push(input_note.id());
}
let mock_chain = builder.build()?;
let expected_nonce_delta = Felt::ONE;
let expected_delta = AccountDelta::new(
account.id(),
expected_storage_patch.clone(),
expected_vault_delta,
None,
expected_nonce_delta,
)?;
let expected_patch = AccountPatch::new(
account.id(),
expected_storage_patch,
expected_vault_patch,
expected_code,
Some(account.nonce() + expected_nonce_delta),
)?;
let delta_run = {
let mut tx = mock_chain
.build_tx_context(account.id(), &input_note_ids, &[])?
.auth_args(emit_delta_args());
if let Some(ref script) = tx_script {
tx = tx.tx_script(script.clone());
}
tx.build()?.execute().await
};
let summary = match delta_run {
Err(TransactionExecutorError::Unauthorized(summary)) => summary,
Err(other) => anyhow::bail!("expected Unauthorized error, got: {other}"),
Ok(_) => anyhow::bail!("expected Unauthorized error, got Ok"),
};
assert_eq!(*summary.account_delta(), expected_delta);
let patch_run_tx = {
let mut tx = mock_chain
.build_tx_context(account.id(), &input_note_ids, &[])?
.auth_args(EMPTY_WORD);
if let Some(script) = tx_script {
tx = tx.tx_script(script);
}
tx.build()?
.execute()
.await
.context("failed to execute transaction (patch run)")?
};
assert_eq!(*patch_run_tx.account_patch(), expected_patch);
Ok(())
}
}
fn emit_delta_args() -> Word {
Word::from([Felt::ONE, ZERO, ZERO, ZERO])
}