diff --git a/miden-tx/src/executor/testing/mod.rs b/miden-tx/src/executor/testing/mod.rs new file mode 100644 index 000000000..6da3f0ea1 --- /dev/null +++ b/miden-tx/src/executor/testing/mod.rs @@ -0,0 +1,126 @@ +// MOCK DATA STORE +// ================================================================================================ + +use alloc::vec::Vec; + +use miden_lib::transaction::TransactionKernel; +use miden_objects::{ + accounts::{ + testing::{ + transaction::{mock_inputs, mock_inputs_with_existing, notes::AssetPreservationStatus}, + MockAccountType, + }, + Account, AccountId, + }, + assembly::ModuleAst, + notes::{Note, NoteId}, + transaction::{ + ChainMmr, InputNote, InputNotes, OutputNote, TransactionArgs, TransactionInputs, + }, + BlockHeader, +}; + +use crate::{DataStore, DataStoreError}; + +#[derive(Clone)] +pub struct MockDataStore { + pub account: Account, + pub block_header: BlockHeader, + pub block_chain: ChainMmr, + pub notes: Vec, + pub tx_args: TransactionArgs, +} + +impl MockDataStore { + pub fn new(asset_preservation_status: AssetPreservationStatus) -> Self { + let (tx_inputs, tx_args) = mock_inputs( + MockAccountType::StandardExisting, + asset_preservation_status, + &TransactionKernel::assembler(), + ); + let (account, _, block_header, block_chain, notes) = tx_inputs.into_parts(); + Self { + account, + block_header, + block_chain, + notes: notes.into_vec(), + tx_args, + } + } + + pub fn with_existing(account: Option, input_notes: Option>) -> Self { + let ( + account, + block_header, + block_chain, + consumed_notes, + _auxiliary_data_inputs, + created_notes, + ) = mock_inputs_with_existing( + MockAccountType::StandardExisting, + AssetPreservationStatus::Preserved, + account, + input_notes, + &TransactionKernel::assembler(), + ); + let output_notes = created_notes.into_iter().filter_map(|note| match note { + OutputNote::Full(note) => Some(note), + OutputNote::Partial(_) => None, + OutputNote::Header(_) => None, + }); + let mut tx_args = TransactionArgs::default(); + tx_args.extend_expected_output_notes(output_notes); + + Self { + account, + block_header, + block_chain, + notes: consumed_notes, + tx_args, + } + } + + pub fn tx_args(&self) -> &TransactionArgs { + &self.tx_args + } +} + +impl Default for MockDataStore { + fn default() -> Self { + Self::new(AssetPreservationStatus::Preserved) + } +} + +impl DataStore for MockDataStore { + fn get_transaction_inputs( + &self, + account_id: AccountId, + block_num: u32, + notes: &[NoteId], + ) -> Result { + assert_eq!(account_id, self.account.id()); + assert_eq!(block_num, self.block_header.block_num()); + assert_eq!(notes.len(), self.notes.len()); + + let notes = self + .notes + .iter() + .filter(|note| notes.contains(¬e.id())) + .cloned() + .collect::>(); + + Ok(TransactionInputs::new( + self.account.clone(), + None, + self.block_header, + self.block_chain.clone(), + InputNotes::new(notes).unwrap(), + ) + .unwrap()) + } + + fn get_account_code(&self, account_id: AccountId) -> Result { + assert_eq!(account_id, self.account.id()); + Ok(self.account.code().module().clone()) + } +} diff --git a/miden-tx/src/host/testing/account_procs.rs b/miden-tx/src/host/testing/account_procs.rs new file mode 100644 index 000000000..aa5b750a1 --- /dev/null +++ b/miden-tx/src/host/testing/account_procs.rs @@ -0,0 +1,63 @@ +use alloc::collections::BTreeMap; + +use miden_lib::transaction::TransactionKernelError; +use miden_objects::accounts::AccountCode; + +use super::{AdviceProvider, Digest, NodeIndex, ProcessState}; + +// ACCOUNT PROCEDURE INDEX MAP +// ================================================================================================ + +/// A map of proc_root |-> proc_index for all known procedures of an account interface. +pub struct AccountProcedureIndexMap(BTreeMap); + +impl AccountProcedureIndexMap { + /// Returns a new [AccountProcedureIndexMap] instantiated with account procedures present in + /// the provided advice provider. + /// + /// This function assumes that the account procedure tree (or a part thereof) is loaded into the + /// Merkle store of the provided advice provider. + pub fn new(account_code_root: Digest, adv_provider: &A) -> Self { + // get the Merkle store with the procedure tree from the advice provider + let proc_store = adv_provider.get_store_subset([account_code_root].iter()); + + // iterate over all possible procedure indexes + let mut result = BTreeMap::new(); + for i in 0..AccountCode::MAX_NUM_PROCEDURES { + let index = NodeIndex::new(AccountCode::PROCEDURE_TREE_DEPTH, i as u64) + .expect("procedure tree index is valid"); + // if the node at the current index does not exist, skip it and try the next node;this + // situation is valid if not all account procedures are loaded into the advice provider + if let Ok(proc_root) = proc_store.get_node(account_code_root, index) { + // if we got an empty digest, this means we got to the end of the procedure list + if proc_root == Digest::default() { + break; + } + result.insert(proc_root, i as u8); + } + } + Self(result) + } + + /// Returns index of the procedure whose root is currently at the top of the operand stack in + /// the provided process. + /// + /// # Errors + /// Returns an error if the procedure at the top of the operand stack is not present in this + /// map. + pub fn get_proc_index( + &self, + process: &S, + ) -> Result { + let proc_root = process.get_stack_word(0).into(); + // mock account method for testing from root context + // TODO: figure out if we can get rid of this + if proc_root == Digest::default() { + return Ok(255); + } + self.0 + .get(&proc_root) + .cloned() + .ok_or(TransactionKernelError::UnknownAccountProcedure(proc_root)) + } +} diff --git a/miden-tx/src/host/testing/mod.rs b/miden-tx/src/host/testing/mod.rs new file mode 100644 index 000000000..099918b7c --- /dev/null +++ b/miden-tx/src/host/testing/mod.rs @@ -0,0 +1,103 @@ +use alloc::string::ToString; + +use miden_lib::transaction::TransactionEvent; +use miden_objects::{ + accounts::{delta::AccountVaultDelta, AccountStub}, + Digest, +}; +use vm_processor::{ + crypto::NodeIndex, AdviceExtractor, AdviceInjector, AdviceInputs, AdviceProvider, AdviceSource, + ContextId, ExecutionError, Host, HostResponse, MemAdviceProvider, ProcessState, +}; + +mod account_procs; +pub mod procedures; +pub mod utils; + +use account_procs::AccountProcedureIndexMap; + +// MOCK HOST +// ================================================================================================ + +/// This is very similar to the TransactionHost in miden-tx. The differences include: +/// - We do not track account delta here. +/// - There is special handling of EMPTY_DIGEST in account procedure index map. +/// - This host uses `MemAdviceProvider` which is instantiated from the passed in advice inputs. +pub struct MockHost { + adv_provider: MemAdviceProvider, + acct_procedure_index_map: AccountProcedureIndexMap, +} + +impl MockHost { + /// Returns a new [MockHost] instance with the provided [AdviceInputs]. + pub fn new(account: AccountStub, advice_inputs: AdviceInputs) -> Self { + let adv_provider: MemAdviceProvider = advice_inputs.into(); + let proc_index_map = AccountProcedureIndexMap::new(account.code_root(), &adv_provider); + Self { + adv_provider, + acct_procedure_index_map: proc_index_map, + } + } + + /// Consumes `self` and returns the advice provider and account vault delta. + pub fn into_parts(self) -> (MemAdviceProvider, AccountVaultDelta) { + (self.adv_provider, AccountVaultDelta::default()) + } + + // EVENT HANDLERS + // -------------------------------------------------------------------------------------------- + + fn on_push_account_procedure_index( + &mut self, + process: &S, + ) -> Result<(), ExecutionError> { + let proc_idx = self + .acct_procedure_index_map + .get_proc_index(process) + .map_err(|err| ExecutionError::EventError(err.to_string()))?; + self.adv_provider.push_stack(AdviceSource::Value(proc_idx.into()))?; + Ok(()) + } +} + +impl Host for MockHost { + fn get_advice( + &mut self, + process: &S, + extractor: AdviceExtractor, + ) -> Result { + self.adv_provider.get_advice(process, &extractor) + } + + fn set_advice( + &mut self, + process: &S, + injector: AdviceInjector, + ) -> Result { + self.adv_provider.set_advice(process, &injector) + } + + fn on_event( + &mut self, + process: &S, + event_id: u32, + ) -> Result { + let event = TransactionEvent::try_from(event_id) + .map_err(|err| ExecutionError::EventError(err.to_string()))?; + + if process.ctx() != ContextId::root() { + return Err(ExecutionError::EventError(format!( + "{event} event can only be emitted from the root context" + ))); + } + + match event { + TransactionEvent::AccountPushProcedureIndex => { + self.on_push_account_procedure_index(process) + }, + _ => Ok(()), + }?; + + Ok(HostResponse::None) + } +} diff --git a/miden-tx/src/host/testing/procedures.rs b/miden-tx/src/host/testing/procedures.rs new file mode 100644 index 000000000..870ef2bbe --- /dev/null +++ b/miden-tx/src/host/testing/procedures.rs @@ -0,0 +1,96 @@ +use alloc::string::String; + +use miden_lib::transaction::memory::{ + CREATED_NOTE_ASSETS_OFFSET, CREATED_NOTE_METADATA_OFFSET, CREATED_NOTE_NUM_ASSETS_OFFSET, + CREATED_NOTE_RECIPIENT_OFFSET, CREATED_NOTE_SECTION_OFFSET, NUM_CREATED_NOTES_PTR, +}; +use miden_objects::{ + accounts::testing::{prepare_assets, prepare_word}, + transaction::{OutputNote, OutputNotes}, +}; + +pub fn output_notes_data_procedure(notes: &OutputNotes) -> String { + let OutputNote::Full(note0) = notes.get_note(0) else { + panic!("Note 0 must be a full note") + }; + let note_0_metadata = prepare_word(¬e0.metadata().into()); + let note_0_recipient = prepare_word(¬e0.recipient().digest()); + let note_0_assets = prepare_assets(note0.assets()); + let note_0_num_assets = 1; + + let OutputNote::Full(note1) = notes.get_note(1) else { + panic!("Note 1 must be a full note") + }; + let note_1_metadata = prepare_word(¬e1.metadata().into()); + let note_1_recipient = prepare_word(¬e1.recipient().digest()); + let note_1_assets = prepare_assets(note1.assets()); + let note_1_num_assets = 1; + + let OutputNote::Full(note2) = notes.get_note(2) else { + panic!("Note 2 must be a full note") + }; + let note_2_metadata = prepare_word(¬e2.metadata().into()); + let note_2_recipient = prepare_word(¬e2.recipient().digest()); + let note_2_assets = prepare_assets(note2.assets()); + let note_2_num_assets = 1; + + // todo: remove this + const NOTE_MEM_SIZE: u32 = 512; + const NOTE_1_OFFSET: u32 = NOTE_MEM_SIZE; + const NOTE_2_OFFSET: u32 = NOTE_MEM_SIZE * 2; + + format!( + " + proc.create_mock_notes + # remove padding from prologue + dropw dropw dropw dropw + + # populate note 0 + push.{note_0_metadata} + push.{CREATED_NOTE_SECTION_OFFSET}.{CREATED_NOTE_METADATA_OFFSET} add mem_storew dropw + + push.{note_0_recipient} + push.{CREATED_NOTE_SECTION_OFFSET}.{CREATED_NOTE_RECIPIENT_OFFSET} add mem_storew dropw + + push.{note_0_num_assets} + push.{CREATED_NOTE_SECTION_OFFSET}.{CREATED_NOTE_NUM_ASSETS_OFFSET} add mem_store + + push.{} + push.{CREATED_NOTE_SECTION_OFFSET}.{CREATED_NOTE_ASSETS_OFFSET} add mem_storew dropw + + # populate note 1 + push.{note_1_metadata} + push.{CREATED_NOTE_SECTION_OFFSET}.{NOTE_1_OFFSET}.{CREATED_NOTE_METADATA_OFFSET} add add mem_storew dropw + + push.{note_1_recipient} + push.{CREATED_NOTE_SECTION_OFFSET}.{NOTE_1_OFFSET}.{CREATED_NOTE_RECIPIENT_OFFSET} add add mem_storew dropw + + push.{note_1_num_assets} + push.{CREATED_NOTE_SECTION_OFFSET}.{NOTE_1_OFFSET}.{CREATED_NOTE_NUM_ASSETS_OFFSET} add add mem_store + + push.{} + push.{CREATED_NOTE_SECTION_OFFSET}.{NOTE_1_OFFSET}.{CREATED_NOTE_ASSETS_OFFSET} add add mem_storew dropw + + # populate note 2 + push.{note_2_metadata} + push.{CREATED_NOTE_SECTION_OFFSET}.{NOTE_2_OFFSET}.{CREATED_NOTE_METADATA_OFFSET} add add mem_storew dropw + + push.{note_2_recipient} + push.{CREATED_NOTE_SECTION_OFFSET}.{NOTE_2_OFFSET}.{CREATED_NOTE_RECIPIENT_OFFSET} add add mem_storew dropw + + push.{note_2_num_assets} + push.{CREATED_NOTE_SECTION_OFFSET}.{NOTE_2_OFFSET}.{CREATED_NOTE_NUM_ASSETS_OFFSET} add add mem_store + + push.{} + push.{CREATED_NOTE_SECTION_OFFSET}.{NOTE_2_OFFSET}.{CREATED_NOTE_ASSETS_OFFSET} add add mem_storew dropw + + # set num created notes + push.{}.{NUM_CREATED_NOTES_PTR} mem_store + end + ", + note_0_assets[0], + note_1_assets[0], + note_2_assets[0], + notes.num_notes() + ) +} diff --git a/miden-tx/src/host/testing/utils.rs b/miden-tx/src/host/testing/utils.rs new file mode 100644 index 000000000..3d8f1eb11 --- /dev/null +++ b/miden-tx/src/host/testing/utils.rs @@ -0,0 +1,133 @@ +#[cfg(feature = "std")] +extern crate std; + +#[cfg(feature = "std")] +use std::{ + fs::File, + io::Read, + path::PathBuf, + string::{String, ToString}, +}; + +use miden_lib::transaction::{memory, ToTransactionKernelInputs, TransactionKernel}; +use miden_objects::transaction::PreparedTransaction; +#[cfg(feature = "std")] +use miden_objects::{ + transaction::{TransactionArgs, TransactionInputs}, + Felt, +}; +use vm_processor::{AdviceInputs, ExecutionError, Process, Word}; +#[cfg(feature = "std")] +use vm_processor::{AdviceProvider, DefaultHost, ExecutionOptions, Host, StackInputs}; + +use super::MockHost; + +// TEST BRACE +// ================================================================================================ + +/// Loads the specified file and append `code` into its end. +#[cfg(feature = "std")] +fn load_file_with_code(imports: &str, code: &str, assembly_file: PathBuf) -> String { + let mut module = String::new(); + File::open(assembly_file).unwrap().read_to_string(&mut module).unwrap(); + let complete_code = format!("{imports}{module}{code}"); + + // This hack is going around issue #686 on miden-vm + complete_code.replace("export", "proc") +} + +/// Inject `code` along side the specified file and run it +pub fn run_tx(tx: &PreparedTransaction) -> Result, ExecutionError> { + run_tx_with_inputs(tx, AdviceInputs::default()) +} + +pub fn run_tx_with_inputs( + tx: &PreparedTransaction, + inputs: AdviceInputs, +) -> Result, ExecutionError> { + let program = tx.program().clone(); + let (stack_inputs, mut advice_inputs) = tx.get_kernel_inputs(); + advice_inputs.extend(inputs); + let host = MockHost::new(tx.account().into(), advice_inputs); + let mut process = Process::new_debug(program.kernel().clone(), stack_inputs, host); + process.execute(&program)?; + Ok(process) +} + +/// Inject `code` along side the specified file and run it +#[cfg(feature = "std")] +pub fn run_within_tx_kernel( + imports: &str, + code: &str, + stack_inputs: StackInputs, + mut adv: A, + file_path: Option, +) -> Result>, ExecutionError> +where + A: AdviceProvider, +{ + // mock account method for testing from root context + adv.insert_into_map(Word::default(), vec![Felt::new(255)]).unwrap(); + + let assembler = TransactionKernel::assembler(); + + let code = match file_path { + Some(file_path) => load_file_with_code(imports, code, file_path), + None => format!("{imports}{code}"), + }; + + let program = assembler.compile(code).unwrap(); + + let host = DefaultHost::new(adv); + let exec_options = ExecutionOptions::default().with_tracing(); + let mut process = Process::new(program.kernel().clone(), stack_inputs, host, exec_options); + process.execute(&program)?; + Ok(process) +} + +/// Inject `code` along side the specified file and run it +#[cfg(feature = "std")] +pub fn run_within_host( + imports: &str, + code: &str, + stack_inputs: StackInputs, + host: H, + file_path: Option, +) -> Result, ExecutionError> { + let assembler = TransactionKernel::assembler(); + let code = match file_path { + Some(file_path) => load_file_with_code(imports, code, file_path), + None => format!("{imports}{code}"), + }; + + let program = assembler.compile(code).unwrap(); + let mut process = + Process::new(program.kernel().clone(), stack_inputs, host, ExecutionOptions::default()); + process.execute(&program)?; + Ok(process) +} + +// TEST HELPERS +// ================================================================================================ +pub fn consumed_note_data_ptr(note_idx: u32) -> memory::MemoryAddress { + memory::CONSUMED_NOTE_DATA_SECTION_OFFSET + note_idx * memory::NOTE_MEM_SIZE +} + +#[cfg(feature = "std")] +pub fn prepare_transaction( + tx_inputs: TransactionInputs, + tx_args: TransactionArgs, + code: &str, + file_path: Option, +) -> PreparedTransaction { + let assembler = TransactionKernel::assembler().with_debug_mode(true); + + let code = match file_path { + Some(file_path) => load_file_with_code("", code, file_path), + None => code.to_string(), + }; + + let program = assembler.compile(code).unwrap(); + + PreparedTransaction::new(program, tx_inputs, tx_args) +} diff --git a/objects/src/accounts/storage/testing.rs b/objects/src/accounts/storage/testing.rs new file mode 100644 index 000000000..d34248f12 --- /dev/null +++ b/objects/src/accounts/storage/testing.rs @@ -0,0 +1,45 @@ +use alloc::vec::Vec; + +use crate::accounts::{AccountStorage, SlotItem, StorageMap}; + +#[derive(Default, Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct AccountStorageBuilder { + items: Vec, + maps: Vec, +} + +/// Builder for an `AccountStorage`, the builder can be configured and used multiple times. +impl AccountStorageBuilder { + pub fn new() -> Self { + Self { items: vec![], maps: vec![] } + } + + pub fn add_item(&mut self, item: SlotItem) -> &mut Self { + self.items.push(item); + self + } + + pub fn add_items>(&mut self, items: I) -> &mut Self { + for item in items.into_iter() { + self.add_item(item); + } + self + } + + #[allow(dead_code)] + pub fn add_map(&mut self, map: StorageMap) -> &mut Self { + self.maps.push(map); + self + } + + #[allow(dead_code)] + pub fn add_maps>(&mut self, maps: I) -> &mut Self { + self.maps.extend(maps); + self + } + + pub fn build(&self) -> AccountStorage { + AccountStorage::new(self.items.clone(), self.maps.clone()).unwrap() + } +} diff --git a/objects/src/accounts/testing/builders/account.rs b/objects/src/accounts/testing/builders/account.rs new file mode 100644 index 000000000..a4aff2ff2 --- /dev/null +++ b/objects/src/accounts/testing/builders/account.rs @@ -0,0 +1,140 @@ +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; + +use assembly::Assembler; +use rand::Rng; + +use super::{ + account_id::{str_to_account_code, AccountIdBuilder}, + AccountBuilderError, +}; +use crate::{ + accounts::{ + storage::testing::AccountStorageBuilder, testing::DEFAULT_ACCOUNT_CODE, Account, + AccountStorage, AccountStorageType, AccountType, SlotItem, + }, + assets::{Asset, AssetVault}, + Felt, Word, ZERO, +}; + +/// Builder for an `Account`, the builder allows for a fluent API to construct an account. Each +/// account needs a unique builder. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct AccountBuilder { + assets: Vec, + storage_builder: AccountStorageBuilder, + code: String, + nonce: Felt, + account_id_builder: AccountIdBuilder, +} + +impl AccountBuilder { + pub fn new(rng: T) -> Self { + Self { + assets: vec![], + storage_builder: AccountStorageBuilder::new(), + code: DEFAULT_ACCOUNT_CODE.to_string(), + nonce: ZERO, + account_id_builder: AccountIdBuilder::new(rng), + } + } + + pub fn add_asset(mut self, asset: Asset) -> Self { + self.assets.push(asset); + self + } + + pub fn add_assets>(mut self, assets: I) -> Self { + for asset in assets.into_iter() { + self.assets.push(asset); + } + self + } + + pub fn add_storage_item(mut self, item: SlotItem) -> Self { + self.storage_builder.add_item(item); + self + } + + pub fn add_storage_items>(mut self, items: I) -> Self { + self.storage_builder.add_items(items); + self + } + + pub fn code>(mut self, code: C) -> Self { + self.code = code.as_ref().to_string(); + self + } + + pub fn nonce(mut self, nonce: Felt) -> Self { + self.nonce = nonce; + self + } + + pub fn account_type(mut self, account_type: AccountType) -> Self { + self.account_id_builder.account_type(account_type); + self + } + + pub fn storage_type(mut self, storage_type: AccountStorageType) -> Self { + self.account_id_builder.storage_type(storage_type); + self + } + + pub fn build(mut self, assembler: &Assembler) -> Result { + let vault = AssetVault::new(&self.assets).map_err(AccountBuilderError::AssetVaultError)?; + let storage = self.storage_builder.build(); + self.account_id_builder.code(&self.code); + self.account_id_builder.storage_root(storage.root()); + let account_id = self.account_id_builder.build(assembler)?; + let account_code = str_to_account_code(&self.code, assembler) + .map_err(AccountBuilderError::AccountError)?; + Ok(Account::from_parts(account_id, vault, storage, account_code, self.nonce)) + } + + /// Build an account using the provided `seed`. + pub fn with_seed( + mut self, + seed: Word, + assembler: &Assembler, + ) -> Result { + let vault = AssetVault::new(&self.assets).map_err(AccountBuilderError::AssetVaultError)?; + let storage = self.storage_builder.build(); + self.account_id_builder.code(&self.code); + self.account_id_builder.storage_root(storage.root()); + let account_id = self.account_id_builder.with_seed(seed, assembler)?; + let account_code = str_to_account_code(&self.code, assembler) + .map_err(AccountBuilderError::AccountError)?; + Ok(Account::from_parts(account_id, vault, storage, account_code, self.nonce)) + } + + /// Build an account using the provided `seed` and `storage`. + /// + /// The storage items added to this builder will added on top of `storage`. + pub fn with_seed_and_storage( + mut self, + seed: Word, + mut storage: AccountStorage, + assembler: &Assembler, + ) -> Result { + let vault = AssetVault::new(&self.assets).map_err(AccountBuilderError::AssetVaultError)?; + let inner_storage = self.storage_builder.build(); + + for (key, value) in inner_storage.slots().leaves() { + if key != 255 { + // don't copy the reserved key + storage.set_item(key as u8, *value).map_err(AccountBuilderError::AccountError)?; + } + } + + self.account_id_builder.code(&self.code); + self.account_id_builder.storage_root(storage.root()); + let account_id = self.account_id_builder.with_seed(seed, assembler)?; + let account_code = str_to_account_code(&self.code, assembler) + .map_err(AccountBuilderError::AccountError)?; + Ok(Account::from_parts(account_id, vault, storage, account_code, self.nonce)) + } +} diff --git a/objects/src/accounts/testing/builders/account_id.rs b/objects/src/accounts/testing/builders/account_id.rs new file mode 100644 index 000000000..abebb80c0 --- /dev/null +++ b/objects/src/accounts/testing/builders/account_id.rs @@ -0,0 +1,124 @@ +use alloc::string::{String, ToString}; + +use assembly::{ast::ModuleAst, Assembler}; +use rand::Rng; + +use super::AccountBuilderError; +use crate::{ + accounts::{ + testing::DEFAULT_ACCOUNT_CODE, AccountCode, AccountId, AccountStorageType, AccountType, + }, + AccountError, Digest, Word, +}; + +/// Builder for an `AccountId`, the builder can be configured and used multiple times. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct AccountIdBuilder { + account_type: AccountType, + storage_type: AccountStorageType, + code: String, + storage_root: Digest, + rng: T, +} + +impl AccountIdBuilder { + pub fn new(rng: T) -> Self { + Self { + account_type: AccountType::RegularAccountUpdatableCode, + storage_type: AccountStorageType::OffChain, + code: DEFAULT_ACCOUNT_CODE.to_string(), + storage_root: Digest::default(), + rng, + } + } + + pub fn account_type(&mut self, account_type: AccountType) -> &mut Self { + self.account_type = account_type; + self + } + + pub fn storage_type(&mut self, storage_type: AccountStorageType) -> &mut Self { + self.storage_type = storage_type; + self + } + + pub fn code>(&mut self, code: C) -> &mut Self { + self.code = code.as_ref().to_string(); + self + } + + pub fn storage_root(&mut self, storage_root: Digest) -> &mut Self { + self.storage_root = storage_root; + self + } + + pub fn build(&mut self, assembler: &Assembler) -> Result { + let (seed, code_root) = account_id_build_details( + &mut self.rng, + &self.code, + self.account_type, + self.storage_type, + self.storage_root, + assembler, + )?; + + AccountId::new(seed, code_root, self.storage_root) + .map_err(AccountBuilderError::AccountError) + } + + pub fn with_seed( + &mut self, + seed: Word, + assembler: &Assembler, + ) -> Result { + let code = str_to_account_code(&self.code, assembler) + .map_err(AccountBuilderError::AccountError)?; + let code_root = code.root(); + + let account_id = AccountId::new(seed, code_root, self.storage_root) + .map_err(AccountBuilderError::AccountError)?; + + if account_id.account_type() != self.account_type { + return Err(AccountBuilderError::SeedAndAccountTypeMismatch); + } + + if account_id.storage_type() != self.storage_type { + return Err(AccountBuilderError::SeedAndOnChainMismatch); + } + + Ok(account_id) + } +} + +// UTILS +// ================================================================================================ + +/// Returns the account's seed and code root. +/// +/// This compiles `code` and performs the proof-of-work to find a valid seed. +pub fn account_id_build_details( + rng: &mut T, + code: &str, + account_type: AccountType, + storage_type: AccountStorageType, + storage_root: Digest, + assembler: &Assembler, +) -> Result<(Word, Digest), AccountBuilderError> { + let init_seed: [u8; 32] = rng.gen(); + let code = str_to_account_code(code, assembler).map_err(AccountBuilderError::AccountError)?; + let code_root = code.root(); + let seed = + AccountId::get_account_seed(init_seed, account_type, storage_type, code_root, storage_root) + .map_err(AccountBuilderError::AccountError)?; + + Ok((seed, code_root)) +} + +pub fn str_to_account_code( + source: &str, + assembler: &Assembler, +) -> Result { + let account_module_ast = ModuleAst::parse(source).unwrap(); + AccountCode::new(account_module_ast, assembler) +} diff --git a/objects/src/accounts/testing/builders/mod.rs b/objects/src/accounts/testing/builders/mod.rs new file mode 100644 index 000000000..8ec3fac87 --- /dev/null +++ b/objects/src/accounts/testing/builders/mod.rs @@ -0,0 +1,28 @@ +use core::fmt::Display; + +use crate::{crypto::merkle::MerkleError, AccountError, AssetVaultError}; + +pub mod account; +pub mod account_id; + +#[derive(Debug)] +pub enum AccountBuilderError { + AccountError(AccountError), + AssetVaultError(AssetVaultError), + MerkleError(MerkleError), + + /// When the created [AccountId] doesn't match the builder's configured [AccountType]. + SeedAndAccountTypeMismatch, + + /// When the created [AccountId] doesn't match the builder's `on_chain` config. + SeedAndOnChainMismatch, +} + +impl Display for AccountBuilderError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for AccountBuilderError {} diff --git a/objects/src/accounts/testing/chain.rs b/objects/src/accounts/testing/chain.rs new file mode 100644 index 000000000..b7aed7889 --- /dev/null +++ b/objects/src/accounts/testing/chain.rs @@ -0,0 +1,639 @@ +use alloc::vec::Vec; +use core::fmt; + +use assembly::Assembler; +use rand::{Rng, SeedableRng}; + +use super::{ + builders::{ + account::AccountBuilder, + account_id::{account_id_build_details, AccountIdBuilder}, + }, + DEFAULT_ACCOUNT_CODE, +}; +use crate::{ + accounts::{ + storage::testing::AccountStorageBuilder, Account, AccountId, AccountStorageType, + AccountType, SlotItem, + }, + assets::{ + testing::{FungibleAssetBuilder, NonFungibleAssetBuilder}, + Asset, + }, + crypto::merkle::{LeafIndex, Mmr, PartialMmr, SimpleSmt, Smt}, + notes::{Note, NoteInclusionProof}, + transaction::{ChainMmr, InputNote}, + BlockHeader, Digest, Word, ACCOUNT_TREE_DEPTH, NOTE_TREE_DEPTH, ZERO, +}; + +/// Initial timestamp value +const TIMESTAMP_START: u32 = 1693348223; +/// Timestamp of timestamp on each new block +const TIMESTAMP_STEP: u32 = 10; + +#[derive(Default, Debug, Clone)] +pub struct Objects { + /// Holds the account and its corresponding seed. + accounts: Vec<(Account, Word)>, + fungible_faucets: Vec<(AccountId, FungibleAssetBuilder)>, + nonfungible_faucets: Vec<(AccountId, NonFungibleAssetBuilder)>, + notes: Vec, + recorded_notes: Vec, + nullifiers: Vec, +} + +impl Objects { + pub fn new() -> Self { + Self { + accounts: vec![], + fungible_faucets: vec![], + nonfungible_faucets: vec![], + notes: vec![], + recorded_notes: vec![], + nullifiers: vec![], + } + } + + /// Update this instance with objects inserted in the chain. + /// + /// This method expects `pending` to be a list of objects in the pending block, and for + /// this instance to be the set of objects added to the chain. Once the pending block is + /// sealed and the auxiliary data is produced (i.e. the notes tree), this method can be + /// called to 1. update the pending objects with the new data 2. move the objects to this + /// container. + pub fn update_with( + &mut self, + pending: &mut Objects, + header: BlockHeader, + notes: &SimpleSmt, + ) { + self.accounts.append(&mut pending.accounts); + self.fungible_faucets.append(&mut pending.fungible_faucets); + self.nonfungible_faucets.append(&mut pending.nonfungible_faucets); + + let recorded_notes = pending.finalize_notes(header, notes); + self.recorded_notes.extend(recorded_notes); + pending.nullifiers.clear(); // nullifiers are saved in the nullifier TSTM + } + + /// Creates a [SimpleSmt] tree from the `notes`. + /// + /// The root of the tree is a commitment to all notes created in the block. The commitment + /// is not for all fields of the [Note] struct, but only for note metadata + core fields of + /// a note (i.e., vault, inputs, script, and serial number). + pub fn build_notes_tree(&self) -> SimpleSmt { + let mut entries = Vec::with_capacity(self.notes.len() * 2); + + entries.extend(self.notes.iter().enumerate().map(|(index, note)| { + let tree_index = (index * 2) as u64; + (tree_index, note.id().into()) + })); + entries.extend(self.notes.iter().enumerate().map(|(index, note)| { + let tree_index = (index * 2 + 1) as u64; + (tree_index, note.metadata().into()) + })); + + SimpleSmt::with_leaves(entries).unwrap() + } + + /// Given the [BlockHeader] and its notedb's [SimpleSmt], set all the [Note]'s proof. + /// + /// Update the [Note]'s proof once the [BlockHeader] has been created. + fn finalize_notes( + &mut self, + header: BlockHeader, + notes: &SimpleSmt, + ) -> Vec { + self.notes + .drain(..) + .enumerate() + .map(|(index, note)| { + let auth_index = LeafIndex::new(index as u64).expect("index bigger than 2**20"); + InputNote::new( + note.clone(), + NoteInclusionProof::new( + header.block_num(), + header.sub_hash(), + header.note_root(), + index as u64, + notes.open(&auth_index).path, + ) + .expect("Invalid data provided to proof constructor"), + ) + }) + .collect::>() + } +} + +/// Structure chain data, used to build necessary openings and to construct [BlockHeader]s. +#[derive(Debug, Clone)] +pub struct MockChain { + /// An append-only structure used to represent the history of blocks produced for this chain. + chain: Mmr, + + /// History of produced blocks. + blocks: Vec, + + /// Tree containing the latest `Nullifier`'s tree. + nullifiers: Smt, + + /// Tree containing the latest hash of each account. + accounts: SimpleSmt, + + /// RNG used to seed builders. + /// + /// This is used to seed the [AccountBuilder] and the [NonFungibleAssetBuilder]. + rng: R, + + /// Builder for new [AccountId]s of faucets. + account_id_builder: AccountIdBuilder, + + /// Objects that have been created and committed to a block. + /// + /// These can be used to perform additional operations on a block. + /// + /// Note: + /// - The [Note]s in this container have the `proof` set. + objects: Objects, + + /// Objects that have been created and are waiting for a block. + /// + /// These objects will become available once the block is sealed. + /// + /// Note: + /// - The [Note]s in this container do not have the `proof` set. + pending_objects: Objects, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Immutable { + No, + Yes, +} + +#[derive(Debug)] +pub enum MockError { + DuplicatedNullifier, + DuplicatedNote, +} + +impl fmt::Display for MockError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for MockError {} + +impl MockChain { + // CONSTRUCTORS + // ---------------------------------------------------------------------------------------- + + pub fn new(mut rng: R) -> Self { + let account_rng = R::from_rng(&mut rng).expect("rng seeding failed"); + let account_id_builder = AccountIdBuilder::new(account_rng); + Self { + chain: Mmr::default(), + blocks: vec![], + nullifiers: Smt::default(), + accounts: SimpleSmt::::new().expect("depth too big for SimpleSmt"), + rng, + account_id_builder, + objects: Objects::new(), + pending_objects: Objects::new(), + } + } + + // BUILDERS + // ---------------------------------------------------------------------------------------- + + /// Creates an [Account] and add to the list of pending objects. + pub fn build_account( + &mut self, + code: C, + storage: S, + assets: A, + immutable: Immutable, + storage_type: AccountStorageType, + assembler: &Assembler, + ) -> AccountId + where + C: AsRef, + S: IntoIterator, + A: IntoIterator, + { + let account_type = match immutable { + Immutable::Yes => AccountType::RegularAccountImmutableCode, + Immutable::No => AccountType::RegularAccountUpdatableCode, + }; + + let storage = AccountStorageBuilder::new().add_items(storage).build(); + + let (seed, _) = account_id_build_details( + &mut self.rng, + code.as_ref(), + account_type, + storage_type, + storage.root(), + assembler, + ) + .unwrap(); + + let rng = R::from_rng(&mut self.rng).expect("rng seeding failed"); + let account = AccountBuilder::new(rng) + .add_assets(assets) + .account_type(account_type) + .storage_type(storage_type) + .code(code) + .with_seed_and_storage(seed, storage, assembler) + .unwrap(); + let account_id = account.id(); + self.pending_objects.accounts.push((account, seed)); + account_id + } + + /// Creates an [Account] using `seed` and add to the list of pending objects. + #[allow(clippy::too_many_arguments)] + pub fn build_account_with_seed( + &mut self, + seed: Word, + code: C, + storage: S, + assets: A, + immutable: Immutable, + storage_type: AccountStorageType, + assembler: &Assembler, + ) -> AccountId + where + C: AsRef, + S: IntoIterator, + A: IntoIterator, + { + let account_type = match immutable { + Immutable::Yes => AccountType::RegularAccountImmutableCode, + Immutable::No => AccountType::RegularAccountUpdatableCode, + }; + + let rng = R::from_rng(&mut self.rng).expect("rng seeding failed"); + let account = AccountBuilder::new(rng) + .add_storage_items(storage) + .add_assets(assets) + .account_type(account_type) + .storage_type(storage_type) + .code(code) + .with_seed(seed, assembler) + .unwrap(); + let account_id = account.id(); + self.pending_objects.accounts.push((account, seed)); + account_id + } + + pub fn build_basic_wallet(&mut self, assembler: &Assembler) -> AccountId { + let account_type = AccountType::RegularAccountUpdatableCode; + let storage = AccountStorageBuilder::new().build(); + let (seed, _) = account_id_build_details( + &mut self.rng, + DEFAULT_ACCOUNT_CODE, + account_type, + AccountStorageType::OnChain, + storage.root(), + assembler, + ) + .unwrap(); + let rng = R::from_rng(&mut self.rng).expect("rng seeding failed"); + let account = AccountBuilder::new(rng) + .account_type(account_type) + .storage_type(AccountStorageType::OnChain) + .code(DEFAULT_ACCOUNT_CODE) + .build(assembler) + .unwrap(); + let account_id = account.id(); + self.pending_objects.accounts.push((account, seed)); + account_id + } + + /// Creates a [AccountId] with type [AccountType::FungibleFaucet] and add to the list of + /// pending objects. + pub fn build_fungible_faucet>( + &mut self, + storage_type: AccountStorageType, + code: C, + storage_root: Digest, + assembler: &Assembler, + ) -> AccountId { + let faucet_id = self + .account_id_builder + .account_type(AccountType::FungibleFaucet) + .storage_type(storage_type) + .code(code) + .storage_root(storage_root) + .build(assembler) + .unwrap(); + let builder = FungibleAssetBuilder::new(faucet_id) + .expect("builder was not configured to create fungible faucets"); + self.pending_objects.fungible_faucets.push((faucet_id, builder)); + faucet_id + } + + /// Creates a [AccountId] with type [AccountType::FungibleFaucet] and add to the list of + /// pending objects. + pub fn build_fungible_faucet_with_seed>( + &mut self, + seed: Word, + storage_type: AccountStorageType, + code: C, + storage_root: Digest, + assembler: &Assembler, + ) -> AccountId { + let faucet_id = self + .account_id_builder + .account_type(AccountType::FungibleFaucet) + .storage_type(storage_type) + .code(code) + .storage_root(storage_root) + .with_seed(seed, assembler) + .unwrap(); + let builder = FungibleAssetBuilder::new(faucet_id) + .expect("builder was not configured to create fungible faucets"); + self.pending_objects.fungible_faucets.push((faucet_id, builder)); + faucet_id + } + + /// Creates a [AccountId] with type [AccountType::NonFungibleFaucet] and add to the list of + /// pending objects. + pub fn build_nonfungible_faucet>( + &mut self, + storage_type: AccountStorageType, + code: C, + storage_root: Digest, + assembler: &Assembler, + ) -> AccountId { + let faucet_id = self + .account_id_builder + .account_type(AccountType::NonFungibleFaucet) + .storage_type(storage_type) + .code(code) + .storage_root(storage_root) + .build(assembler) + .unwrap(); + let rng = R::from_rng(&mut self.rng).expect("rng seeding failed"); + let builder = NonFungibleAssetBuilder::new(faucet_id, rng) + .expect("builder was not configured to build nonfungible faucets"); + self.pending_objects.nonfungible_faucets.push((faucet_id, builder)); + faucet_id + } + + /// Creates a [AccountId] with type [AccountType::NonFungibleFaucet] and add to the list of + /// pending objects. + pub fn build_nonfungible_faucet_with_seed>( + &mut self, + seed: Word, + storage_type: AccountStorageType, + code: C, + storage_root: Digest, + assembler: &Assembler, + ) -> AccountId { + let faucet_id = self + .account_id_builder + .account_type(AccountType::NonFungibleFaucet) + .storage_type(storage_type) + .code(code) + .storage_root(storage_root) + .with_seed(seed, assembler) + .unwrap(); + let rng = R::from_rng(&mut self.rng).expect("rng seeding failed"); + let builder = NonFungibleAssetBuilder::new(faucet_id, rng) + .expect("builder was not configured to build nonfungible faucets"); + self.pending_objects.nonfungible_faucets.push((faucet_id, builder)); + faucet_id + } + + /// Creates [FungibleAsset] from the fungible faucet at position `faucet_pos`. + pub fn build_fungible_asset(&mut self, faucet_pos: usize, amount: u64) -> Asset { + self.objects.fungible_faucets[faucet_pos] + .1 + .amount(amount) + .unwrap() + .build() + .map(|v| v.into()) + .unwrap() + } + + /// Creates [NonFungibleAsset] from the nonfungible faucet at position `faucet_pos`. + pub fn build_nonfungible_asset(&mut self, faucet_pos: usize) -> Asset { + self.objects.nonfungible_faucets[faucet_pos] + .1 + .build() + .map(|v| v.into()) + .unwrap() + } + + fn check_nullifier_unknown(&self, nullifier: Digest) { + assert!(self.pending_objects.nullifiers.iter().any(|e| *e == nullifier)); + assert!(self.nullifiers.get_value(&nullifier) != Smt::EMPTY_VALUE) + } + + // MODIFIERS + // ---------------------------------------------------------------------------------------- + + /// Creates the next block. + /// + /// This will also make all the objects currently pending available for use. + pub fn seal_block(&mut self) -> BlockHeader { + let block_num: u32 = self.blocks.len().try_into().expect("usize to u32 failed"); + + for (account, _seed) in self.pending_objects.accounts.iter() { + self.accounts.insert(account.id().into(), account.hash().into()); + } + for (account, _seed) in self.objects.accounts.iter() { + self.accounts.insert(account.id().into(), account.hash().into()); + } + + // TODO: + // - resetting the nullifier tree once defined at the protocol level. + // - inserting only nullifier from transactions included in the batches, once the batch + // kernel has been implemented. + for nullifier in self.pending_objects.nullifiers.iter() { + self.nullifiers.insert(*nullifier, [block_num.into(), ZERO, ZERO, ZERO]); + } + let notes = self.pending_objects.build_notes_tree(); + + let version = 0; + let previous = self.blocks.last(); + let peaks = self.chain.peaks(self.chain.forest()).unwrap(); + let chain_root: Digest = peaks.hash_peaks(); + let account_root = self.accounts.root(); + let prev_hash = previous.map_or(Digest::default(), |header| header.hash()); + let nullifier_root = self.nullifiers.root(); + let note_root = notes.root(); + let timestamp = + previous.map_or(TIMESTAMP_START, |header| header.timestamp() + TIMESTAMP_STEP); + + // TODO: Set batch_root and proof_hash to the correct values once the kernel is + // available. + let batch_root = Digest::default(); + let proof_hash = Digest::default(); + + let header = BlockHeader::new( + version, + prev_hash, + block_num, + chain_root, + account_root, + nullifier_root, + note_root, + batch_root, + proof_hash, + timestamp, + ); + + self.blocks.push(header); + self.chain.add(header.hash()); + self.objects.update_with(&mut self.pending_objects, header, ¬es); + + header + } + + /// Mark a [Note] as produced by inserting into the block. + pub fn add_note(&mut self, note: Note) -> Result<(), MockError> { + if self.pending_objects.notes.iter().any(|e| e.id() == note.id()) { + return Err(MockError::DuplicatedNote); + } + + // The check below works because the notes can not be added directly to the + // [BlockHeader], so we don't have to iterate over the known headers and check for + // inclusion proofs. + if self.objects.recorded_notes.iter().any(|e| e.id() == note.id()) { + return Err(MockError::DuplicatedNote); + } + + self.check_nullifier_unknown(note.nullifier().inner()); + self.pending_objects.notes.push(note); + Ok(()) + } + + /// Mark a [Note] as consumed by inserting its nullifier into the block. + pub fn add_nullifier(&mut self, nullifier: Digest) -> Result<(), MockError> { + self.check_nullifier_unknown(nullifier); + self.pending_objects.nullifiers.push(nullifier); + Ok(()) + } + + /// Add a known [Account] to the mock chain. + pub fn add_account(&mut self, account: Account, seed: Word) { + assert!( + !self.pending_objects.accounts.iter().any(|(a, _)| a.id() == account.id()), + "Found duplicated AccountId" + ); + self.pending_objects.accounts.push((account, seed)); + } + + // ACCESSORS + // ---------------------------------------------------------------------------------------- + + /// Get the latest [ChainMmr]. + pub fn chain(&self) -> ChainMmr { + mmr_to_chain_mmr(&self.chain, &self.blocks) + } + + /// Get a reference to [BlockHeader] with `block_number`. + pub fn block_header(&self, block_number: usize) -> &BlockHeader { + &self.blocks[block_number] + } + + /// Get a reference to the nullifier tree. + pub fn nullifiers(&self) -> &Smt { + &self.nullifiers + } + + /// Get the [AccountId] of the nth fungible faucet. + pub fn fungible(&self, faucet_pos: usize) -> AccountId { + self.objects.fungible_faucets[faucet_pos].0 + } + + /// Get the [AccountId] of the nth nonfungible faucet. + pub fn nonfungible(&self, faucet_pos: usize) -> AccountId { + self.objects.nonfungible_faucets[faucet_pos].0 + } + + /// Get a mutable reference to nth [Account]. + pub fn account_mut(&mut self, pos: usize) -> &mut Account { + &mut self.objects.accounts[pos].0 + } + + /// Get the [Account]'s corresponding seed. + pub fn account_seed(&mut self, pos: usize) -> Word { + self.objects.accounts[pos].1 + } +} + +pub fn mock_chain_data(consumed_notes: Vec) -> (ChainMmr, Vec) { + let mut note_trees = Vec::new(); + + // TODO: Consider how to better represent note authentication data. + // we use the index for both the block number and the leaf index in the note tree + for (index, note) in consumed_notes.iter().enumerate() { + let smt_entries = vec![(index as u64, note.authentication_hash().into())]; + let smt = SimpleSmt::::with_leaves(smt_entries).unwrap(); + note_trees.push(smt); + } + + let mut note_tree_iter = note_trees.iter(); + + // create a dummy chain of block headers + let block_chain = vec![ + BlockHeader::mock(0, None, note_tree_iter.next().map(|x| x.root()), &[]), + BlockHeader::mock(1, None, note_tree_iter.next().map(|x| x.root()), &[]), + BlockHeader::mock(2, None, note_tree_iter.next().map(|x| x.root()), &[]), + BlockHeader::mock(3, None, note_tree_iter.next().map(|x| x.root()), &[]), + ]; + + // instantiate and populate MMR + let mut mmr = Mmr::default(); + for block_header in block_chain.iter() { + mmr.add(block_header.hash()) + } + let chain_mmr = mmr_to_chain_mmr(&mmr, &block_chain); + + // set origin for consumed notes using chain and block data + let recorded_notes = consumed_notes + .into_iter() + .enumerate() + .map(|(index, note)| { + let block_header = &block_chain[index]; + let auth_index = LeafIndex::new(index as u64).unwrap(); + + InputNote::new( + note, + NoteInclusionProof::new( + block_header.block_num(), + block_header.sub_hash(), + block_header.note_root(), + index as u64, + note_trees[index].open(&auth_index).path, + ) + .unwrap(), + ) + }) + .collect::>(); + + (chain_mmr, recorded_notes) +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Converts the MMR into partial MMR by copying all leaves from MMR to partial MMR. +fn mmr_to_chain_mmr(mmr: &Mmr, blocks: &[BlockHeader]) -> ChainMmr { + let num_leaves = mmr.forest(); + let mut partial_mmr = PartialMmr::from_peaks(mmr.peaks(mmr.forest()).unwrap()); + + for i in 0..num_leaves { + let node = mmr.get(i).unwrap(); + let path = mmr.open(i, mmr.forest()).unwrap().merkle_path; + partial_mmr.track(i, node, &path).unwrap(); + } + + ChainMmr::new(partial_mmr, blocks.to_vec()).unwrap() +} diff --git a/objects/src/accounts/testing/mod.rs b/objects/src/accounts/testing/mod.rs new file mode 100644 index 000000000..53e3de6b0 --- /dev/null +++ b/objects/src/accounts/testing/mod.rs @@ -0,0 +1,513 @@ +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; + +use super::{ + account_id::testing::ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, + code::testing::make_account_code, AccountDelta, AccountStorageDelta, AccountVaultDelta, + StorageSlotType, +}; +use crate::{ + accounts::{ + account_id::testing::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN, + }, + get_account_seed_single, Account, AccountCode, AccountId, AccountStorage, + AccountStorageType, AccountType, SlotItem, StorageMap, StorageSlot, + }, + assembly::{Assembler, ModuleAst}, + assets::{Asset, AssetVault, FungibleAsset}, + crypto::{hash::rpo::RpoDigest, merkle::Smt}, + notes::NoteAssets, + testing::{ + constants::{FUNGIBLE_ASSET_AMOUNT, FUNGIBLE_FAUCET_INITIAL_BALANCE}, + non_fungible_asset, non_fungible_asset_2, + }, + Felt, FieldElement, Word, ZERO, +}; + +pub mod builders; +pub mod chain; +pub mod transaction; + +// ACCOUNT STORAGE +// ================================================================================================ + +pub const FAUCET_STORAGE_DATA_SLOT: u8 = 254; + +pub const STORAGE_INDEX_0: u8 = 20; +pub const STORAGE_VALUE_0: Word = [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]; +pub const STORAGE_INDEX_1: u8 = 30; +pub const STORAGE_VALUE_1: Word = [Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]; + +pub const STORAGE_INDEX_2: u8 = 40; +pub const STORAGE_LEAVES_2: [(RpoDigest, Word); 2] = [ + ( + RpoDigest::new([Felt::new(101), Felt::new(102), Felt::new(103), Felt::new(104)]), + [Felt::new(1_u64), Felt::new(2_u64), Felt::new(3_u64), Felt::new(4_u64)], + ), + ( + RpoDigest::new([Felt::new(105), Felt::new(106), Felt::new(107), Felt::new(108)]), + [Felt::new(5_u64), Felt::new(6_u64), Felt::new(7_u64), Felt::new(8_u64)], + ), +]; + +pub fn storage_item_0() -> SlotItem { + SlotItem { + index: STORAGE_INDEX_0, + slot: StorageSlot::new_value(STORAGE_VALUE_0), + } +} + +pub fn storage_item_1() -> SlotItem { + SlotItem { + index: STORAGE_INDEX_1, + slot: StorageSlot::new_value(STORAGE_VALUE_1), + } +} + +pub fn storage_map_2() -> StorageMap { + StorageMap::with_entries(STORAGE_LEAVES_2).unwrap() +} + +pub fn storage_item_2() -> SlotItem { + SlotItem { + index: STORAGE_INDEX_2, + slot: StorageSlot::new_map(Word::from(storage_map_2().root())), + } +} + +/// Creates an [AssetVault] with 4 assets. +/// +/// The ids of the assets added to the vault are defined by the following constants: +/// +/// - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN +/// - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1 +/// - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2 +/// - ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN +/// +fn mock_account_vault() -> AssetVault { + let faucet_id: AccountId = ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN.try_into().unwrap(); + let fungible_asset = + Asset::Fungible(FungibleAsset::new(faucet_id, FUNGIBLE_ASSET_AMOUNT).unwrap()); + + let faucet_id_1: AccountId = ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1.try_into().unwrap(); + let fungible_asset_1 = + Asset::Fungible(FungibleAsset::new(faucet_id_1, FUNGIBLE_ASSET_AMOUNT).unwrap()); + + let faucet_id_2: AccountId = ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2.try_into().unwrap(); + let fungible_asset_2 = + Asset::Fungible(FungibleAsset::new(faucet_id_2, FUNGIBLE_ASSET_AMOUNT).unwrap()); + + let non_fungible_asset = non_fungible_asset(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN); + AssetVault::new(&[fungible_asset, fungible_asset_1, fungible_asset_2, non_fungible_asset]) + .unwrap() +} + +pub fn mock_account_storage() -> AccountStorage { + // create account storage + AccountStorage::new( + vec![storage_item_0(), storage_item_1(), storage_item_2()], + vec![storage_map_2()], + ) + .unwrap() +} + +// The MAST root of the default account's interface. Use these constants to interact with the +// account's procedures. +const MASTS: [&str; 11] = [ + "0x74de7e94e5afc71e608f590c139ac51f446fc694da83f93d968b019d1d2b7306", + "0xf5f4a93b873d3dc236539d5566d245ac5e5bc6be75fc5af4235a52fe091077ae", + "0xd765111e22479256e87a57eaf3a27479d19cc876c9a715ee6c262e0a0d47a2ac", + "0x17b326d5403115afccc0727efa72bd929bfdc7bbf284c7c28a7aadade5d4cc9d", + "0x6682a0e0f4e49820e5c547f1b60a82cb326a56c972999e36bf6d45459393ac87", + "0x73c14f65d2bab6f52eafc4397e104b3ab22a470f6b5cbc86d4aa4d3978c8b7d4", + "0x49fee714925e6b287136494465184a84495cedb35fce3ab3a13f68ad48751596", + "0xfe4b6f0a485393583f5b6de9edca2f133f3e7ad0c3e631eadd0d18e89bfdbfe0", + "0x976ff83372d5e5f4618927de2f64ebc14cd0a2c651ddded4ba0485973aa03caa", + "0xff06b90f849c4b262cbfbea67042c4ea017ea0e9c558848a951d44b23370bec5", + "0x8ef0092134469a1330e3c468f57c7f085ce611645d09cc7516c786fefc71d794", +]; +pub const ACCOUNT_SEND_ASSET_MAST_ROOT: &str = MASTS[1]; +pub const ACCOUNT_INCR_NONCE_MAST_ROOT: &str = MASTS[2]; +pub const ACCOUNT_SET_ITEM_MAST_ROOT: &str = MASTS[3]; +pub const ACCOUNT_SET_MAP_ITEM_MAST_ROOT: &str = MASTS[4]; +pub const ACCOUNT_SET_CODE_MAST_ROOT: &str = MASTS[5]; +pub const ACCOUNT_CREATE_NOTE_MAST_ROOT: &str = MASTS[6]; +pub const ACCOUNT_ADD_ASSET_TO_NOTE_MAST_ROOT: &str = MASTS[7]; +pub const ACCOUNT_REMOVE_ASSET_MAST_ROOT: &str = MASTS[8]; +pub const ACCOUNT_ACCOUNT_PROCEDURE_1_MAST_ROOT: &str = MASTS[9]; +pub const ACCOUNT_ACCOUNT_PROCEDURE_2_MAST_ROOT: &str = MASTS[10]; + +// ACCOUNT ASSEMBLY CODE +// ================================================================================================ + +pub const DEFAULT_ACCOUNT_CODE: &str = " + use.miden::contracts::wallets::basic->basic_wallet + use.miden::contracts::auth::basic->basic_eoa + + export.basic_wallet::receive_asset + export.basic_wallet::send_asset + export.basic_eoa::auth_tx_rpo_falcon512 +"; + +pub const DEFAULT_AUTH_SCRIPT: &str = " + use.miden::contracts::auth::basic->auth_tx + + begin + call.auth_tx::auth_tx_rpo_falcon512 + end +"; + +pub fn mock_account_code(assembler: &Assembler) -> AccountCode { + let account_code = "\ + use.miden::account + use.miden::tx + use.miden::contracts::wallets::basic->wallet + + # acct proc 0 + export.wallet::receive_asset + # acct proc 1 + export.wallet::send_asset + + # acct proc 2 + export.incr_nonce + push.0 swap + # => [value, 0] + + exec.account::incr_nonce + # => [0] + end + + # acct proc 3 + export.set_item + exec.account::set_item + # => [R', V, 0, 0, 0] + + movup.8 drop movup.8 drop movup.8 drop + # => [R', V] + end + + # acct proc 4 + export.set_map_item + exec.account::set_map_item + # => [R', V, 0, 0, 0] + + movup.8 drop movup.8 drop movup.8 drop + # => [R', V] + end + + # acct proc 5 + export.set_code + padw swapw + # => [CODE_ROOT, 0, 0, 0, 0] + + exec.account::set_code + # => [0, 0, 0, 0] + end + + # acct proc 6 + export.create_note + exec.tx::create_note + # => [ptr] + + swap drop swap drop swap drop + end + + # acct proc 7 + export.add_asset_to_note + exec.tx::add_asset_to_note + # => [ptr] + + swap drop swap drop swap drop + end + + # acct proc 8 + export.remove_asset + exec.account::remove_asset + # => [ASSET] + end + + # acct proc 9 + export.account_procedure_1 + push.1.2 + add + end + + # acct proc 10 + export.account_procedure_2 + push.2.1 + sub + end + "; + let account_module_ast = ModuleAst::parse(account_code).unwrap(); + let code = AccountCode::new(account_module_ast, assembler).unwrap(); + + // Ensures the mast root constants match the latest version of the code. + // + // The constants will change if the library code changes, and need to be updated so that the + // tests will work properly. If these asserts fail, copy the value of the code (the left + // value), into the constants. + // + // Comparing all the values together, in case multiple of them change, a single test run will + // detect it. + let current = [ + code.procedures()[0].to_hex(), + code.procedures()[1].to_hex(), + code.procedures()[2].to_hex(), + code.procedures()[3].to_hex(), + code.procedures()[4].to_hex(), + code.procedures()[5].to_hex(), + code.procedures()[6].to_hex(), + code.procedures()[7].to_hex(), + code.procedures()[8].to_hex(), + code.procedures()[9].to_hex(), + code.procedures()[10].to_hex(), + ]; + assert!(current == MASTS, "const MASTS: [&str; 11] = {:?};", current); + + code +} + +// MOCK ACCOUNT +// ================================================================================================ + +#[derive(Debug, PartialEq)] +pub enum MockAccountType { + StandardNew, + StandardExisting, + FungibleFaucet { + acct_id: u64, + nonce: Felt, + empty_reserved_slot: bool, + }, + NonFungibleFaucet { + acct_id: u64, + nonce: Felt, + empty_reserved_slot: bool, + }, +} + +pub fn mock_new_account(assembler: &Assembler) -> Account { + let (acct_id, _account_seed) = + generate_account_seed(AccountSeedType::RegularAccountUpdatableCodeOffChain, assembler); + let account_storage = mock_account_storage(); + let account_code = mock_account_code(assembler); + Account::from_parts(acct_id, AssetVault::default(), account_storage, account_code, ZERO) +} + +pub fn mock_account(account_id: u64, nonce: Felt, account_code: AccountCode) -> Account { + let account_storage = mock_account_storage(); + let account_vault = mock_account_vault(); + let account_id = AccountId::try_from(account_id).unwrap(); + Account::from_parts(account_id, account_vault, account_storage, account_code, nonce) +} + +// MOCK FAUCET +// ================================================================================================ + +pub fn mock_fungible_faucet( + account_id: u64, + nonce: Felt, + empty_reserved_slot: bool, + assembler: &Assembler, +) -> Account { + let initial_balance = if empty_reserved_slot { + ZERO + } else { + Felt::new(FUNGIBLE_FAUCET_INITIAL_BALANCE) + }; + let account_storage = AccountStorage::new( + vec![SlotItem { + index: FAUCET_STORAGE_DATA_SLOT, + slot: StorageSlot::new_value([ZERO, ZERO, ZERO, initial_balance]), + }], + vec![], + ) + .unwrap(); + let account_id = AccountId::try_from(account_id).unwrap(); + let account_code = mock_account_code(assembler); + Account::from_parts(account_id, AssetVault::default(), account_storage, account_code, nonce) +} + +pub fn mock_non_fungible_faucet( + account_id: u64, + nonce: Felt, + empty_reserved_slot: bool, + assembler: &Assembler, +) -> Account { + let entires = match empty_reserved_slot { + true => vec![], + false => vec![( + Word::from(non_fungible_asset_2(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN)).into(), + non_fungible_asset_2(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN).into(), + )], + }; + + // construct nft tree + let nft_tree = Smt::with_entries(entires).unwrap(); + + // TODO: add nft tree data to account storage? + + let account_storage = AccountStorage::new( + vec![SlotItem { + index: FAUCET_STORAGE_DATA_SLOT, + slot: StorageSlot::new_map(*nft_tree.root()), + }], + vec![], + ) + .unwrap(); + let account_id = AccountId::try_from(account_id).unwrap(); + let account_code = mock_account_code(assembler); + Account::from_parts(account_id, AssetVault::default(), account_storage, account_code, nonce) +} + +// ACCOUNT SEED GENERATION +// ================================================================================================ + +pub enum AccountSeedType { + FungibleFaucetInvalidInitialBalance, + FungibleFaucetValidInitialBalance, + NonFungibleFaucetInvalidReservedSlot, + NonFungibleFaucetValidReservedSlot, + RegularAccountUpdatableCodeOnChain, + RegularAccountUpdatableCodeOffChain, +} + +/// Returns the account id and seed for the specified account type. +pub fn generate_account_seed( + account_seed_type: AccountSeedType, + assembler: &Assembler, +) -> (AccountId, Word) { + let init_seed: [u8; 32] = Default::default(); + + let (account, account_type) = match account_seed_type { + AccountSeedType::FungibleFaucetInvalidInitialBalance => ( + mock_fungible_faucet( + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN, + ZERO, + false, + assembler, + ), + AccountType::FungibleFaucet, + ), + AccountSeedType::FungibleFaucetValidInitialBalance => ( + mock_fungible_faucet( + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN, + ZERO, + true, + assembler, + ), + AccountType::FungibleFaucet, + ), + AccountSeedType::NonFungibleFaucetInvalidReservedSlot => ( + mock_non_fungible_faucet( + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN, + ZERO, + false, + assembler, + ), + AccountType::NonFungibleFaucet, + ), + AccountSeedType::NonFungibleFaucetValidReservedSlot => ( + mock_non_fungible_faucet( + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN, + ZERO, + true, + assembler, + ), + AccountType::NonFungibleFaucet, + ), + AccountSeedType::RegularAccountUpdatableCodeOnChain => ( + mock_account( + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + Felt::ONE, + mock_account_code(assembler), + ), + AccountType::RegularAccountUpdatableCode, + ), + AccountSeedType::RegularAccountUpdatableCodeOffChain => ( + mock_account( + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + Felt::ONE, + mock_account_code(assembler), + ), + AccountType::RegularAccountUpdatableCode, + ), + }; + + let seed = get_account_seed_single( + init_seed, + account_type, + AccountStorageType::OnChain, + account.code().root(), + account.storage().root(), + ) + .unwrap(); + + let account_id = AccountId::new(seed, account.code().root(), account.storage().root()).unwrap(); + + (account_id, seed) +} + +// UTILITIES +// -------------------------------------------------------------------------------------------- + +pub fn build_account(assets: Vec, nonce: Felt, storage_items: Vec) -> Account { + let id = AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN).unwrap(); + let code = make_account_code(); + + // build account data + let vault = AssetVault::new(&assets).unwrap(); + + let slot_type = StorageSlotType::Value { value_arity: 0 }; + let slot_items: Vec = storage_items + .into_iter() + .enumerate() + .map(|(index, item)| SlotItem { + index: index as u8, + slot: StorageSlot { slot_type, value: item }, + }) + .collect(); + let storage = AccountStorage::new(slot_items, vec![]).unwrap(); + + Account::from_parts(id, vault, storage, code, nonce) +} + +pub fn build_account_delta( + added_assets: Vec, + removed_assets: Vec, + nonce: Felt, + storage_delta: AccountStorageDelta, +) -> AccountDelta { + let vault_delta = AccountVaultDelta { added_assets, removed_assets }; + AccountDelta::new(storage_delta, vault_delta, Some(nonce)).unwrap() +} + +pub fn build_assets() -> (Asset, Asset) { + let faucet_id_0 = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); + let asset_0: Asset = FungibleAsset::new(faucet_id_0, 123).unwrap().into(); + + let faucet_id_1 = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2).unwrap(); + let asset_1: Asset = FungibleAsset::new(faucet_id_1, 345).unwrap().into(); + + (asset_0, asset_1) +} + +pub fn prepare_word(word: &Word) -> String { + word.iter().map(|x| x.as_int().to_string()).collect::>().join(".") +} + +pub fn prepare_assets(note_assets: &NoteAssets) -> Vec { + let mut assets = Vec::new(); + for &asset in note_assets.iter() { + let asset_word: Word = asset.into(); + let asset_str = prepare_word(&asset_word); + assets.push(asset_str); + } + assets +} diff --git a/objects/src/accounts/testing/transaction.rs b/objects/src/accounts/testing/transaction.rs new file mode 100644 index 000000000..28d6204e3 --- /dev/null +++ b/objects/src/accounts/testing/transaction.rs @@ -0,0 +1,178 @@ +use alloc::vec::Vec; + +use assembly::Assembler; +use vm_core::code_blocks::CodeBlock; +use vm_processor::{AdviceInputs, Operation, Program, Word}; + +use self::notes::{mock_notes, AssetPreservationStatus}; +use super::{ + chain::mock_chain_data, mock_account, mock_account_code, mock_fungible_faucet, + mock_new_account, mock_non_fungible_faucet, MockAccountType, +}; +use crate::{ + accounts::{ + account_id::testing::ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, Account, + AccountDelta, + }, + notes::Note, + transaction::{ + ChainMmr, ExecutedTransaction, InputNote, InputNotes, OutputNote, OutputNotes, + TransactionArgs, TransactionInputs, TransactionOutputs, + }, + BlockHeader, Felt, FieldElement, ZERO, +}; + +pub mod notes; + +pub fn mock_inputs( + account_type: MockAccountType, + asset_preservation: AssetPreservationStatus, + assembler: &Assembler, +) -> (TransactionInputs, TransactionArgs) { + mock_inputs_with_account_seed(account_type, asset_preservation, None, assembler) +} + +pub fn mock_inputs_with_account_seed( + account_type: MockAccountType, + asset_preservation: AssetPreservationStatus, + account_seed: Option, + assembler: &Assembler, +) -> (TransactionInputs, TransactionArgs) { + let account = match account_type { + MockAccountType::StandardNew => mock_new_account(assembler), + MockAccountType::StandardExisting => mock_account( + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + Felt::ONE, + mock_account_code(assembler), + ), + MockAccountType::FungibleFaucet { acct_id, nonce, empty_reserved_slot } => { + mock_fungible_faucet(acct_id, nonce, empty_reserved_slot, assembler) + }, + MockAccountType::NonFungibleFaucet { acct_id, nonce, empty_reserved_slot } => { + mock_non_fungible_faucet(acct_id, nonce, empty_reserved_slot, assembler) + }, + }; + + let (input_notes, output_notes) = mock_notes(assembler, &asset_preservation); + + let (chain_mmr, recorded_notes) = mock_chain_data(input_notes); + + let block_header = + BlockHeader::mock(4, Some(chain_mmr.peaks().hash_peaks()), None, &[account.clone()]); + + let input_notes = InputNotes::new(recorded_notes).unwrap(); + let tx_inputs = + TransactionInputs::new(account, account_seed, block_header, chain_mmr, input_notes) + .unwrap(); + + let output_notes = output_notes.into_iter().filter_map(|n| match n { + OutputNote::Full(note) => Some(note), + OutputNote::Partial(_) => None, + OutputNote::Header(_) => None, + }); + let mut tx_args = TransactionArgs::default(); + tx_args.extend_expected_output_notes(output_notes); + + (tx_inputs, tx_args) +} + +pub fn mock_inputs_with_existing( + account_type: MockAccountType, + asset_preservation: AssetPreservationStatus, + account: Option, + consumed_notes_from: Option>, + assembler: &Assembler, +) -> (Account, BlockHeader, ChainMmr, Vec, AdviceInputs, Vec) { + let auxiliary_data = AdviceInputs::default(); + + let account = match account_type { + MockAccountType::StandardNew => mock_new_account(assembler), + MockAccountType::StandardExisting => account.unwrap_or(mock_account( + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + Felt::ONE, + mock_account_code(assembler), + )), + MockAccountType::FungibleFaucet { acct_id, nonce, empty_reserved_slot } => { + account.unwrap_or(mock_fungible_faucet(acct_id, nonce, empty_reserved_slot, assembler)) + }, + MockAccountType::NonFungibleFaucet { acct_id, nonce, empty_reserved_slot } => { + mock_non_fungible_faucet(acct_id, nonce, empty_reserved_slot, assembler) + }, + }; + + let (mut consumed_notes, created_notes) = mock_notes(assembler, &asset_preservation); + if let Some(ref notes) = consumed_notes_from { + consumed_notes = notes.to_vec(); + } + + let (chain_mmr, recorded_notes) = mock_chain_data(consumed_notes); + + let block_header = + BlockHeader::mock(4, Some(chain_mmr.peaks().hash_peaks()), None, &[account.clone()]); + + (account, block_header, chain_mmr, recorded_notes, auxiliary_data, created_notes) +} + +pub fn mock_executed_tx( + asset_preservation: AssetPreservationStatus, + assembler: &Assembler, +) -> ExecutedTransaction { + let initial_account = mock_account( + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + Felt::ONE, + mock_account_code(assembler), + ); + + // nonce incremented by 1 + let final_account = mock_account( + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + Felt::new(2), + initial_account.code().clone(), + ); + + let (input_notes, output_notes) = mock_notes(assembler, &asset_preservation); + let (block_chain, input_notes) = mock_chain_data(input_notes); + + let block_header = BlockHeader::mock( + 4, + Some(block_chain.peaks().hash_peaks()), + None, + &[initial_account.clone()], + ); + + let tx_inputs = TransactionInputs::new( + initial_account, + None, + block_header, + block_chain, + InputNotes::new(input_notes).unwrap(), + ) + .unwrap(); + + let mut tx_args: TransactionArgs = TransactionArgs::default(); + for note in &output_notes { + if let OutputNote::Full(note) = note { + tx_args.add_expected_output_note(note); + } + } + + let tx_outputs = TransactionOutputs { + account: final_account.into(), + output_notes: OutputNotes::new(output_notes).unwrap(), + }; + + let program = build_dummy_tx_program(); + let account_delta = AccountDelta::default(); + let advice_witness = AdviceInputs::default(); + + ExecutedTransaction::new(program, tx_inputs, tx_outputs, account_delta, tx_args, advice_witness) +} + +// HELPER FUNCTIONS +// ================================================================================================ + +fn build_dummy_tx_program() -> Program { + let operations = vec![Operation::Push(ZERO), Operation::Drop]; + let span = CodeBlock::new_span(operations); + Program::new(span) +} diff --git a/objects/src/accounts/testing/transaction/notes.rs b/objects/src/accounts/testing/transaction/notes.rs new file mode 100644 index 000000000..1644a6d67 --- /dev/null +++ b/objects/src/accounts/testing/transaction/notes.rs @@ -0,0 +1,257 @@ +use alloc::vec::Vec; + +use crate::{ + accounts::{ + account_id::testing::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, + ACCOUNT_ID_SENDER, + }, + testing::{ + prepare_assets, prepare_word, ACCOUNT_ADD_ASSET_TO_NOTE_MAST_ROOT, + ACCOUNT_CREATE_NOTE_MAST_ROOT, + }, + AccountId, + }, + assembly::{Assembler, ProgramAst}, + assets::{Asset, FungibleAsset}, + notes::{Note, NoteAssets, NoteInputs, NoteMetadata, NoteRecipient, NoteScript, NoteType}, + testing::{ + constants::{CONSUMED_ASSET_1_AMOUNT, CONSUMED_ASSET_2_AMOUNT, CONSUMED_ASSET_3_AMOUNT}, + non_fungible_asset_2, + }, + transaction::OutputNote, + Felt, Word, ZERO, +}; + +pub enum AssetPreservationStatus { + TooFewInput, + Preserved, + PreservedWithAccountVaultDelta, + TooManyFungibleInput, + TooManyNonFungibleInput, +} + +pub fn mock_notes( + assembler: &Assembler, + asset_preservation: &AssetPreservationStatus, +) -> (Vec, Vec) { + let mut serial_num_gen = SerialNumGenerator::new(); + + // ACCOUNT IDS + // -------------------------------------------------------------------------------------------- + let sender = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap(); + let faucet_id_1 = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1).unwrap(); + let faucet_id_2 = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2).unwrap(); + let faucet_id_3 = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3).unwrap(); + + // ASSETS + // -------------------------------------------------------------------------------------------- + let fungible_asset_1: Asset = + FungibleAsset::new(faucet_id_1, CONSUMED_ASSET_1_AMOUNT).unwrap().into(); + let fungible_asset_2: Asset = + FungibleAsset::new(faucet_id_2, CONSUMED_ASSET_2_AMOUNT).unwrap().into(); + let fungible_asset_3: Asset = + FungibleAsset::new(faucet_id_3, CONSUMED_ASSET_3_AMOUNT).unwrap().into(); + + // CREATED NOTES + // -------------------------------------------------------------------------------------------- + let note_program_ast = ProgramAst::parse("begin push.1 drop end").unwrap(); + let (note_script, _) = NoteScript::new(note_program_ast, assembler).unwrap(); + + let inputs = NoteInputs::new(vec![Felt::new(1)]).unwrap(); + let vault = NoteAssets::new(vec![fungible_asset_1]).unwrap(); + let metadata = NoteMetadata::new(sender, NoteType::Public, 0.into(), ZERO).unwrap(); + let recipient = NoteRecipient::new(serial_num_gen.next(), note_script.clone(), inputs); + let created_note_1 = Note::new(vault, metadata, recipient); + + let inputs = NoteInputs::new(vec![Felt::new(2)]).unwrap(); + let vault = NoteAssets::new(vec![fungible_asset_2]).unwrap(); + let metadata = NoteMetadata::new(sender, NoteType::Public, 0.into(), ZERO).unwrap(); + let recipient = NoteRecipient::new(serial_num_gen.next(), note_script.clone(), inputs); + let created_note_2 = Note::new(vault, metadata, recipient); + + let inputs = NoteInputs::new(vec![Felt::new(3)]).unwrap(); + let vault = NoteAssets::new(vec![fungible_asset_3]).unwrap(); + let metadata = NoteMetadata::new(sender, NoteType::Public, 0.into(), ZERO).unwrap(); + let recipient = NoteRecipient::new(serial_num_gen.next(), note_script.clone(), inputs); + let created_note_3 = Note::new(vault, metadata, recipient); + + // CONSUMED NOTES + // -------------------------------------------------------------------------------------------- + let note_1_script_src = format!( + "\ + begin + # create note 0 + push.{recipient0} + push.{PUBLIC_NOTE} + push.{tag0} + # MAST root of the `create_note` mock account procedure + call.{ACCOUNT_CREATE_NOTE_MAST_ROOT} + + push.{asset0} movup.4 + call.{ACCOUNT_ADD_ASSET_TO_NOTE_MAST_ROOT} + drop drop dropw dropw + + # create note 1 + push.{recipient1} + push.{PUBLIC_NOTE} + push.{tag1} + # MAST root of the `create_note` mock account procedure + call.{ACCOUNT_CREATE_NOTE_MAST_ROOT} + + push.{asset1} movup.4 + call.{ACCOUNT_ADD_ASSET_TO_NOTE_MAST_ROOT} + drop drop dropw dropw + end + ", + PUBLIC_NOTE = NoteType::Public as u8, + recipient0 = prepare_word(&created_note_1.recipient().digest()), + tag0 = created_note_1.metadata().tag(), + asset0 = prepare_assets(created_note_1.assets())[0], + recipient1 = prepare_word(&created_note_2.recipient().digest()), + tag1 = created_note_2.metadata().tag(), + asset1 = prepare_assets(created_note_2.assets())[0], + ); + let note_1_script_ast = ProgramAst::parse(¬e_1_script_src).unwrap(); + let (note_1_script, _) = NoteScript::new(note_1_script_ast, assembler).unwrap(); + let metadata = NoteMetadata::new(sender, NoteType::Public, 0.into(), ZERO).unwrap(); + let vault = NoteAssets::new(vec![fungible_asset_1]).unwrap(); + let inputs = NoteInputs::new(vec![Felt::new(1)]).unwrap(); + let recipient = NoteRecipient::new(serial_num_gen.next(), note_1_script, inputs); + let consumed_note_1 = Note::new(vault, metadata, recipient); + + let note_2_script_src = format!( + "\ + begin + # create note 2 + push.{recipient} + push.{PUBLIC_NOTE} + push.{tag} + # MAST root of the `create_note` mock account procedure + call.{ACCOUNT_CREATE_NOTE_MAST_ROOT} + + push.{asset} movup.4 + call.{ACCOUNT_ADD_ASSET_TO_NOTE_MAST_ROOT} + drop drop dropw dropw + end + ", + PUBLIC_NOTE = NoteType::Public as u8, + recipient = prepare_word(&created_note_3.recipient().digest()), + tag = created_note_3.metadata().tag(), + asset = prepare_assets(created_note_3.assets())[0], + ); + + let note_2_script_ast = ProgramAst::parse(¬e_2_script_src).unwrap(); + let (note_2_script, _) = NoteScript::new(note_2_script_ast, assembler).unwrap(); + let metadata = NoteMetadata::new(sender, NoteType::Public, 0.into(), ZERO).unwrap(); + let vault = NoteAssets::new(vec![fungible_asset_2, fungible_asset_3]).unwrap(); + let inputs = NoteInputs::new(vec![Felt::new(2)]).unwrap(); + let recipient = NoteRecipient::new(serial_num_gen.next(), note_2_script, inputs); + let consumed_note_2 = Note::new(vault, metadata, recipient); + + let note_3_script_ast = ProgramAst::parse("begin push.1 drop end").unwrap(); + let (note_3_script, _) = NoteScript::new(note_3_script_ast, assembler).unwrap(); + let metadata = NoteMetadata::new(sender, NoteType::Public, 0.into(), ZERO).unwrap(); + let vault = NoteAssets::new(vec![fungible_asset_2, fungible_asset_3]).unwrap(); + let inputs = NoteInputs::new(vec![Felt::new(2)]).unwrap(); + let recipient = NoteRecipient::new(serial_num_gen.next(), note_3_script, inputs); + let consumed_note_3 = Note::new(vault, metadata, recipient); + + let note_4_script_ast = ProgramAst::parse("begin push.1 drop end").unwrap(); + let (note_4_script, _) = NoteScript::new(note_4_script_ast, assembler).unwrap(); + let metadata = NoteMetadata::new(sender, NoteType::Public, 0.into(), ZERO).unwrap(); + let vault = + NoteAssets::new(vec![non_fungible_asset_2(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN)]) + .unwrap(); + let inputs = NoteInputs::new(vec![Felt::new(1)]).unwrap(); + let recipient = NoteRecipient::new(serial_num_gen.next(), note_4_script, inputs); + let consumed_note_4 = Note::new(vault, metadata, recipient); + + // note that changes the account vault + let note_5_script_ast = ProgramAst::parse( + "\ + use.miden::note + use.miden::contracts::wallets::basic->wallet + + begin + # read the assets to memory + push.0 exec.note::get_assets + # => [num_assets, dest_ptr] + + # assert the number of assets is 3 + push.3 assert_eq + # => [dest_ptr] + + # add the first asset to the vault + padw dup.4 mem_loadw call.wallet::receive_asset dropw + # => [dest_ptr] + + # add the second asset to the vault + push.1 add padw dup.4 mem_loadw call.wallet::receive_asset dropw + # => [dest_ptr+1] + + # add the third asset to the vault + push.1 add padw movup.4 mem_loadw call.wallet::receive_asset dropw + # => [] + end + ", + ) + .unwrap(); + let (note_5_script, _) = NoteScript::new(note_5_script_ast, assembler).unwrap(); + + let metadata = NoteMetadata::new(sender, NoteType::Public, 0.into(), ZERO).unwrap(); + let vault = NoteAssets::new(vec![ + fungible_asset_1, + fungible_asset_3, + non_fungible_asset_2(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN), + ]) + .unwrap(); + let inputs = NoteInputs::new(vec![]).unwrap(); + let recipient = NoteRecipient::new(serial_num_gen.next(), note_5_script, inputs); + let consumed_note_5 = Note::new(vault, metadata, recipient); + + let consumed_notes = match asset_preservation { + AssetPreservationStatus::TooFewInput => vec![consumed_note_1], + AssetPreservationStatus::Preserved => { + vec![consumed_note_1, consumed_note_2] + }, + AssetPreservationStatus::PreservedWithAccountVaultDelta => { + vec![consumed_note_1, consumed_note_2, consumed_note_5] + }, + AssetPreservationStatus::TooManyFungibleInput => { + vec![consumed_note_1, consumed_note_2, consumed_note_3] + }, + AssetPreservationStatus::TooManyNonFungibleInput => { + vec![consumed_note_1, consumed_note_2, consumed_note_4] + }, + }; + let created_notes = vec![ + OutputNote::Full(created_note_1), + OutputNote::Full(created_note_2), + OutputNote::Full(created_note_3), + ]; + + (consumed_notes, created_notes) +} + +struct SerialNumGenerator { + state: u64, +} + +impl SerialNumGenerator { + pub fn new() -> Self { + Self { state: 0 } + } + + pub fn next(&mut self) -> Word { + let serial_num = [ + Felt::new(self.state), + Felt::new(self.state + 1), + Felt::new(self.state + 2), + Felt::new(self.state + 3), + ]; + self.state += 4; + serial_num + } +} diff --git a/objects/src/assets/testing.rs b/objects/src/assets/testing.rs new file mode 100644 index 000000000..4852a67c6 --- /dev/null +++ b/objects/src/assets/testing.rs @@ -0,0 +1,88 @@ +use rand::{distributions::Standard, Rng}; + +use super::FungibleAsset; +use crate::{ + accounts::{AccountId, AccountType}, + assets::{NonFungibleAsset, NonFungibleAssetDetails}, + AssetError, +}; + +/// Builder for an `NonFungibleAssetDetails`, the builder can be configured and used multiplied times. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct NonFungibleAssetDetailsBuilder { + faucet_id: AccountId, + rng: T, +} + +/// Builder for an `FungibleAsset`, the builder can be configured and used multiplied times. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct FungibleAssetBuilder { + faucet_id: AccountId, + amount: u64, +} + +impl NonFungibleAssetDetailsBuilder { + pub fn new(faucet_id: AccountId, rng: T) -> Result { + if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) { + return Err(AssetError::not_a_non_fungible_faucet_id(faucet_id)); + } + + Ok(Self { faucet_id, rng }) + } + + pub fn build(&mut self) -> Result { + let data = (&mut self.rng).sample_iter(Standard).take(5).collect(); + NonFungibleAssetDetails::new(self.faucet_id, data) + } +} + +/// Builder for an `NonFungibleAsset`, the builder can be configured and used multiplied times. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct NonFungibleAssetBuilder { + details_builder: NonFungibleAssetDetailsBuilder, +} + +impl NonFungibleAssetBuilder { + pub fn new(faucet_id: AccountId, rng: T) -> Result { + let details_builder = NonFungibleAssetDetailsBuilder::new(faucet_id, rng)?; + Ok(Self { details_builder }) + } + + pub fn build(&mut self) -> Result { + let details = self.details_builder.build()?; + NonFungibleAsset::new(&details) + } +} + +impl FungibleAssetBuilder { + pub const DEFAULT_AMOUNT: u64 = 10; + + pub fn new(faucet_id: AccountId) -> Result { + let account_type = faucet_id.account_type(); + if !matches!(account_type, AccountType::FungibleFaucet) { + return Err(AssetError::not_a_fungible_faucet_id(faucet_id, account_type)); + } + + Ok(Self { faucet_id, amount: Self::DEFAULT_AMOUNT }) + } + + pub fn amount(&mut self, amount: u64) -> Result<&mut Self, AssetError> { + if amount > FungibleAsset::MAX_AMOUNT { + return Err(AssetError::amount_too_big(amount)); + } + + self.amount = amount; + Ok(self) + } + + pub fn with_amount(&self, amount: u64) -> Result { + FungibleAsset::new(self.faucet_id, amount) + } + + pub fn build(&self) -> Result { + FungibleAsset::new(self.faucet_id, self.amount) + } +} diff --git a/objects/src/notes/testing.rs b/objects/src/notes/testing.rs new file mode 100644 index 000000000..e5de3250b --- /dev/null +++ b/objects/src/notes/testing.rs @@ -0,0 +1,101 @@ +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; + +use assembly::Assembler; +use rand::Rng; + +use crate::{ + accounts::AccountId, + assembly::ProgramAst, + assets::Asset, + notes::{ + Note, NoteAssets, NoteInclusionProof, NoteInputs, NoteMetadata, NoteRecipient, NoteScript, + NoteTag, NoteType, + }, + Felt, NoteError, Word, ZERO, +}; + +const DEFAULT_NOTE_CODE: &str = "\ +begin +end +"; + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct NoteBuilder { + sender: AccountId, + inputs: Vec, + assets: Vec, + note_type: NoteType, + serial_num: Word, + tag: NoteTag, + code: String, + proof: Option, + aux: Felt, +} + +impl NoteBuilder { + pub fn new(sender: AccountId, mut rng: T) -> Self { + let serial_num = [ + Felt::new(rng.gen()), + Felt::new(rng.gen()), + Felt::new(rng.gen()), + Felt::new(rng.gen()), + ]; + + Self { + sender, + inputs: vec![], + assets: vec![], + note_type: NoteType::Public, + serial_num, + tag: 0.into(), + code: DEFAULT_NOTE_CODE.to_string(), + proof: None, + aux: ZERO, + } + } + + pub fn note_inputs(mut self, inputs: Vec) -> Result { + NoteInputs::new(inputs.to_vec())?; + self.inputs = inputs; + Ok(self) + } + + pub fn add_asset(mut self, asset: Asset) -> Self { + self.assets.push(asset); + self + } + + pub fn tag(mut self, tag: u32) -> Self { + self.tag = tag.into(); + self + } + + pub fn code>(mut self, code: S) -> Self { + self.code = code.as_ref().to_string(); + self + } + + pub fn proof(mut self, proof: NoteInclusionProof) -> Self { + self.proof = Some(proof); + self + } + + pub fn aux(mut self, aux: Felt) -> Self { + self.aux = aux; + self + } + + pub fn build(self, assembler: Assembler) -> Result { + let note_ast = ProgramAst::parse(&self.code).unwrap(); + let (note_script, _) = NoteScript::new(note_ast, &assembler)?; + let vault = NoteAssets::new(self.assets)?; + let metadata = NoteMetadata::new(self.sender, self.note_type, self.tag, self.aux)?; + let inputs = NoteInputs::new(self.inputs)?; + let recipient = NoteRecipient::new(self.serial_num, note_script, inputs); + Ok(Note::new(vault, metadata, recipient)) + } +} diff --git a/objects/src/testing.rs b/objects/src/testing.rs new file mode 100644 index 000000000..6e52ba6e3 --- /dev/null +++ b/objects/src/testing.rs @@ -0,0 +1,49 @@ +use self::constants::NON_FUNGIBLE_ASSET_DATA_2; +use crate::{ + accounts::AccountId, + assets::{Asset, NonFungibleAsset, NonFungibleAssetDetails}, +}; + +pub mod constants { + use vm_core::Felt; + + pub const FUNGIBLE_ASSET_AMOUNT: u64 = 100; + pub const FUNGIBLE_FAUCET_INITIAL_BALANCE: u64 = 50000; + + pub const MIN_PROOF_SECURITY_LEVEL: u32 = 96; + + pub const CONSUMED_ASSET_1_AMOUNT: u64 = 100; + pub const CONSUMED_ASSET_2_AMOUNT: u64 = 200; + pub const CONSUMED_ASSET_3_AMOUNT: u64 = 300; + pub const CONSUMED_ASSET_4_AMOUNT: u64 = 100; + + pub const NON_FUNGIBLE_ASSET_DATA: [u8; 4] = [1, 2, 3, 4]; + pub const NON_FUNGIBLE_ASSET_DATA_2: [u8; 4] = [5, 6, 7, 8]; + + pub const CHILD_ROOT_PARENT_LEAF_INDEX: u8 = 10; + pub const CHILD_SMT_DEPTH: u8 = 64; + pub const CHILD_STORAGE_INDEX_0: u64 = 40; + pub const CHILD_STORAGE_VALUE_0: [Felt; 4] = + [Felt::new(11), Felt::new(12), Felt::new(13), Felt::new(14)]; +} + +pub fn non_fungible_asset(account_id: u64) -> Asset { + let non_fungible_asset_details = NonFungibleAssetDetails::new( + AccountId::try_from(account_id).unwrap(), + constants::NON_FUNGIBLE_ASSET_DATA.to_vec(), + ) + .unwrap(); + let non_fungible_asset = NonFungibleAsset::new(&non_fungible_asset_details).unwrap(); + Asset::NonFungible(non_fungible_asset) +} + +pub fn non_fungible_asset_2(account_id: u64) -> Asset { + let non_fungible_asset_2_details: NonFungibleAssetDetails = NonFungibleAssetDetails::new( + AccountId::try_from(account_id).unwrap(), + NON_FUNGIBLE_ASSET_DATA_2.to_vec(), + ) + .unwrap(); + let non_fungible_asset_2: NonFungibleAsset = + NonFungibleAsset::new(&non_fungible_asset_2_details).unwrap(); + Asset::NonFungible(non_fungible_asset_2) +}