diff --git a/CHANGELOG.md b/CHANGELOG.md index abfd44d16..063caa420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * [BREAKING] Removed the transaction script root output from the transaction kernel (#608). * [BREAKING] Refactored account update details, moved `Block` to `miden-objects` (#618, #621). +* Introduce the `miden-bench-tx` crate used for transactions benchmarking (#577). ## 0.2.3 (2024-04-26) - `miden-tx` crate only diff --git a/Cargo.lock b/Cargo.lock index 290feff0f..30693cb97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -597,6 +597,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "miden-bench-tx" +version = "0.1.0" +dependencies = [ + "miden-lib", + "miden-mock", + "miden-objects", + "miden-processor", + "miden-tx", + "serde", + "serde_json", +] + [[package]] name = "miden-core" version = "0.9.1" @@ -1055,6 +1068,7 @@ version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ + "indexmap", "itoa", "ryu", "serde", diff --git a/Cargo.toml b/Cargo.toml index d156177bd..65f8813b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" -members = [ +members = [ + "bench-tx", "miden-lib", "miden-tx", "mock", diff --git a/Makefile.toml b/Makefile.toml index a7347e3ac..96751e6aa 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -96,9 +96,16 @@ args = ["build", "--release"] [tasks.build-no-std] description = "Build using no-std" command = "cargo" -args = ["build", "--no-default-features", "--target", "wasm32-unknown-unknown", "--workspace", "--exclude", "miden-mock"] +args = ["build", "--no-default-features", "--target", "wasm32-unknown-unknown", "--workspace", "--exclude", "miden-mock", "--exclude", "miden-bench-tx"] -# --- utilities ---------------------------------------------------------------------------------------- +# --- benchmarking -------------------------------------------------------------------------------- +[tasks.bench-tx] +description = "Run all available transaction benchmarks" +workspace = false +command = "cargo" +args = ["run", "--bin", "bench-tx"] + +# --- utilities ----------------------------------------------------------------------------------- [tasks.watch] description = "Watch for changes and rebuild" workspace = false diff --git a/bench-tx/Cargo.toml b/bench-tx/Cargo.toml new file mode 100644 index 000000000..6ce821fcd --- /dev/null +++ b/bench-tx/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "miden-bench-tx" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[[bin]] +name = "bench-tx" +path = "src/main.rs" + +[dependencies] +miden-lib = { package = "miden-lib", path = "../miden-lib", version = "0.3" } +miden-objects = { package = "miden-objects", path = "../objects", version = "0.3" } +miden-tx = { package = "miden-tx", path = "../miden-tx", version = "0.3" } +mock = { package = "miden-mock", path = "../mock" } +serde = { package = "serde", version = "1.0" } +serde_json = { package = "serde_json", version = "1.0", features = ["preserve_order"] } +vm-processor = { workspace = true } diff --git a/bench-tx/README.md b/bench-tx/README.md new file mode 100644 index 000000000..425d4f89a --- /dev/null +++ b/bench-tx/README.md @@ -0,0 +1,24 @@ +# Miden transactions benchmark + +This crate contains an executable used for benchmarking transactions. + +For each transaction, data is collected on the number of cycles required to complete: +- Prologue +- All notes processing +- Each note execution +- Transaction script processing +- Epilogue + +## Usage + +To run the benchmark you can use [cargo-make](https://github.com/sagiegurari/cargo-make) with the following command present in our [Makefile.toml](Makefile.toml): + +```shell +cargo make bench-tx +``` + +Results of the benchmark are stored in the [bench-tx.json](bench-tx.json) file. + +## License + +This project is [MIT licensed](../LICENSE). diff --git a/bench-tx/bench-tx.json b/bench-tx/bench-tx.json new file mode 100644 index 000000000..647eacdf8 --- /dev/null +++ b/bench-tx/bench-tx.json @@ -0,0 +1,21 @@ +{ + "simple": { + "prologue": 3644, + "notes_processing": 1151, + "note_execution": { + "0x47b2fbff8a3f09e40343238e7b15c8918d7c63e570fd1b3c904ada458c4d74bd": 392, + "0x8a55c3531cdd5725aa805475093ed3006c6773b71a008e8ca840da8364a67cd6": 715 + }, + "tx_script_processing": 32, + "epilogue": 2222 + }, + "p2id": { + "prologue": 2004, + "notes_processing": 920, + "note_execution": { + "0xb9fa30eb43d80d579be02dc004338e06b5ad565e81e0bac11a94ab01abfdd40a": 883 + }, + "tx_script_processing": 88209, + "epilogue": 272 + } +} \ No newline at end of file diff --git a/bench-tx/src/main.rs b/bench-tx/src/main.rs new file mode 100644 index 000000000..197bc5dd2 --- /dev/null +++ b/bench-tx/src/main.rs @@ -0,0 +1,163 @@ +use core::fmt; +use std::{ + fs::{read_to_string, write, File}, + io::Write, + path::Path, +}; + +use miden_lib::{ + notes::create_p2id_note, transaction::ToTransactionKernelInputs, utils::Serializable, +}; +use miden_objects::{ + accounts::AccountId, + assembly::ProgramAst, + assets::{Asset, FungibleAsset}, + crypto::{dsa::rpo_falcon512::SecretKey, rand::RpoRandomCoin}, + notes::NoteType, + transaction::TransactionArgs, + Felt, +}; +use miden_tx::{TransactionExecutor, TransactionHost, TransactionProgress}; +use vm_processor::{ExecutionOptions, RecAdviceProvider, Word}; + +mod utils; +use utils::{ + get_account_with_default_account_code, write_bench_results_to_json, MockDataStore, String, + ToString, Vec, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, ACCOUNT_ID_SENDER, DEFAULT_AUTH_SCRIPT, +}; + +pub enum Benchmark { + Simple, + P2ID, +} + +impl fmt::Display for Benchmark { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Benchmark::Simple => write!(f, "simple"), + Benchmark::P2ID => write!(f, "p2id"), + } + } +} + +fn main() -> Result<(), String> { + // create a template file for benchmark results + let path = Path::new("bench-tx/bench-tx.json"); + let mut file = File::create(path).map_err(|e| e.to_string())?; + file.write_all(b"{}").map_err(|e| e.to_string())?; + + // run all available benchmarks + let benchmark_results = vec![ + (Benchmark::Simple, benchmark_default_tx()?), + (Benchmark::P2ID, benchmark_p2id()?), + ]; + + // store benchmark results in the JSON file + write_bench_results_to_json(path, benchmark_results)?; + + Ok(()) +} + +// BENCHMARKS +// ================================================================================================ + +/// Runs the default transaction with empty transaction script and two default notes. +pub fn benchmark_default_tx() -> Result { + let data_store = MockDataStore::default(); + let mut executor = TransactionExecutor::new(data_store.clone()).with_tracing(); + + let account_id = data_store.account.id(); + executor.load_account(account_id).map_err(|e| e.to_string())?; + + let block_ref = data_store.block_header.block_num(); + let note_ids = data_store.notes.iter().map(|note| note.id()).collect::>(); + + let transaction = executor + .prepare_transaction(account_id, block_ref, ¬e_ids, data_store.tx_args().clone()) + .map_err(|e| e.to_string())?; + + let (stack_inputs, advice_inputs) = transaction.get_kernel_inputs(); + let advice_recorder: RecAdviceProvider = advice_inputs.into(); + let mut host = TransactionHost::new(transaction.account().into(), advice_recorder); + + vm_processor::execute( + transaction.program(), + stack_inputs, + &mut host, + ExecutionOptions::default().with_tracing(), + ) + .map_err(|e| e.to_string())?; + + Ok(host.tx_progress().clone()) +} + +/// Runs the transaction which consumes a P2ID note into a basic wallet. +pub fn benchmark_p2id() -> Result { + // Create assets + let faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); + let fungible_asset: Asset = FungibleAsset::new(faucet_id, 100).unwrap().into(); + + // Create sender and target account + let sender_account_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap(); + + let target_account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN).unwrap(); + let sec_key = SecretKey::new(); + let target_pub_key: Word = sec_key.public_key().into(); + let mut pk_sk_bytes = sec_key.to_bytes(); + pk_sk_bytes.append(&mut target_pub_key.to_bytes()); + let target_sk_pk_felt: Vec = + pk_sk_bytes.iter().map(|a| Felt::new(*a as u64)).collect::>(); + let target_account = + get_account_with_default_account_code(target_account_id, target_pub_key, None); + + // Create the note + let note = create_p2id_note( + sender_account_id, + target_account_id, + vec![fungible_asset], + NoteType::Public, + RpoRandomCoin::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]), + ) + .unwrap(); + + let data_store = + MockDataStore::with_existing(Some(target_account.clone()), Some(vec![note.clone()])); + + let mut executor = TransactionExecutor::new(data_store.clone()).with_tracing(); + executor.load_account(target_account_id).unwrap(); + + let block_ref = data_store.block_header.block_num(); + let note_ids = data_store.notes.iter().map(|note| note.id()).collect::>(); + + let tx_script_code = ProgramAst::parse(DEFAULT_AUTH_SCRIPT).unwrap(); + + let tx_script_target = executor + .compile_tx_script( + tx_script_code.clone(), + vec![(target_pub_key, target_sk_pk_felt)], + vec![], + ) + .unwrap(); + let tx_args_target = TransactionArgs::with_tx_script(tx_script_target); + + // execute transaction + let transaction = executor + .prepare_transaction(target_account_id, block_ref, ¬e_ids, tx_args_target) + .map_err(|e| e.to_string())?; + + let (stack_inputs, advice_inputs) = transaction.get_kernel_inputs(); + let advice_recorder: RecAdviceProvider = advice_inputs.into(); + let mut host = TransactionHost::new(transaction.account().into(), advice_recorder); + + vm_processor::execute( + transaction.program(), + stack_inputs, + &mut host, + ExecutionOptions::default().with_tracing(), + ) + .map_err(|e| e.to_string())?; + + Ok(host.tx_progress().clone()) +} diff --git a/bench-tx/src/utils.rs b/bench-tx/src/utils.rs new file mode 100644 index 000000000..6cd792879 --- /dev/null +++ b/bench-tx/src/utils.rs @@ -0,0 +1,245 @@ +extern crate alloc; +pub use alloc::{ + collections::BTreeMap, + string::{String, ToString}, + vec::Vec, +}; + +use miden_lib::transaction::TransactionKernel; +use miden_objects::{ + accounts::{Account, AccountCode, AccountId, AccountStorage, SlotItem, StorageSlot}, + assembly::ModuleAst, + assets::{Asset, AssetVault}, + notes::{Note, NoteId}, + transaction::{ChainMmr, InputNote, InputNotes, OutputNote, TransactionArgs}, + BlockHeader, Felt, Word, +}; +use miden_tx::{DataStore, DataStoreError, TransactionInputs, TransactionProgress}; +use mock::mock::{ + account::MockAccountType, + notes::AssetPreservationStatus, + transaction::{mock_inputs, mock_inputs_with_existing}, +}; +use serde::Serialize; +use serde_json::{from_str, to_string_pretty, Value}; + +use super::{read_to_string, write, Benchmark, Path}; + +// CONSTANTS +// ================================================================================================ + +pub const ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN: u64 = 0x200000000000001F; // 2305843009213693983 +pub const ACCOUNT_ID_SENDER: u64 = 0x800000000000001F; // 9223372036854775839 +pub const ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN: u64 = 0x900000000000003F; // 10376293541461622847 + +pub const DEFAULT_AUTH_SCRIPT: &str = " + use.miden::contracts::auth::basic->auth_tx + + begin + call.auth_tx::auth_tx_rpo_falcon512 + end +"; + +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 +"; + +// MOCK DATA STORE +// ================================================================================================ + +#[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: AssetPreservationStatus) -> Self { + let (tx_inputs, tx_args) = + mock_inputs(MockAccountType::StandardExisting, asset_preservation); + 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, + ); + let output_notes = created_notes.into_iter().filter_map(|note| match note { + OutputNote::Public(note) => Some(note), + OutputNote::Private(_) => 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()) + } +} + +// TRANSACTION BENCHMARK +// ================================================================================================ + +#[derive(Serialize)] +pub struct TransactionBenchmark { + prologue: Option, + notes_processing: Option, + note_execution: BTreeMap>, + tx_script_processing: Option, + epilogue: Option, +} + +impl From for TransactionBenchmark { + fn from(tx_progress: TransactionProgress) -> Self { + let prologue = tx_progress.prologue().len(); + + let notes_processing = tx_progress.notes_processing().len(); + + let mut note_execution = BTreeMap::new(); + tx_progress.note_execution().iter().for_each(|(note_id, interval)| { + note_execution.insert(note_id.to_hex(), interval.len()); + }); + + let tx_script_processing = tx_progress.tx_script_processing().len(); + + let epilogue = tx_progress.epilogue().len(); + + Self { + prologue, + notes_processing, + note_execution, + tx_script_processing, + epilogue, + } + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +pub fn get_account_with_default_account_code( + account_id: AccountId, + public_key: Word, + assets: Option, +) -> Account { + let account_code_src = DEFAULT_ACCOUNT_CODE; + let account_code_ast = ModuleAst::parse(account_code_src).unwrap(); + let account_assembler = TransactionKernel::assembler(); + + let account_code = AccountCode::new(account_code_ast.clone(), &account_assembler).unwrap(); + let account_storage = AccountStorage::new( + vec![SlotItem { + index: 0, + slot: StorageSlot::new_value(public_key), + }], + vec![], + ) + .unwrap(); + + let account_vault = match assets { + Some(asset) => AssetVault::new(&[asset]).unwrap(), + None => AssetVault::new(&[]).unwrap(), + }; + + Account::new(account_id, account_vault, account_storage, account_code, Felt::new(1)) +} + +pub fn write_bench_results_to_json( + path: &Path, + tx_benchmarks: Vec<(Benchmark, TransactionProgress)>, +) -> Result<(), String> { + // convert benchmark file internals to the JSON Value + let benchmark_file = read_to_string(path).map_err(|e| e.to_string())?; + let mut benchmark_json: Value = from_str(&benchmark_file).map_err(|e| e.to_string())?; + + // fill becnhmarks JSON with results of each benchmark + for (bench_type, tx_progress) in tx_benchmarks { + let tx_benchmark = TransactionBenchmark::from(tx_progress); + let tx_benchmark_json = serde_json::to_value(tx_benchmark).map_err(|e| e.to_string())?; + + benchmark_json[bench_type.to_string()] = tx_benchmark_json; + } + + // write the becnhmarks JSON to the results file + write( + path, + to_string_pretty(&benchmark_json).expect("failed to convert json to String"), + ) + .map_err(|e| e.to_string())?; + + Ok(()) +} diff --git a/miden-lib/asm/kernels/transaction/main.masm b/miden-lib/asm/kernels/transaction/main.masm index e6b0e3727..86268fa3a 100644 --- a/miden-lib/asm/kernels/transaction/main.masm +++ b/miden-lib/asm/kernels/transaction/main.masm @@ -5,6 +5,37 @@ use.miden::kernels::tx::memory use.miden::kernels::tx::note use.miden::kernels::tx::prologue +# TRACES +# ================================================================================================= + +# Trace emitted to signal that an execution of the transaction prologue has started. +const.PROLOGUE_START=131072 +# Trace emitted to signal that an execution of the transaction prologue has ended. +const.PROLOGUE_END=131073 + +# Trace emitted to signal that the notes processing has started. +const.NOTES_PROCESSING_START=131074 +# Trace emitted to signal that the notes processing has ended. +const.NOTES_PROCESSING_END=131075 + +# Trace emitted to signal that the note consuming has started. +const.NOTE_EXECUTION_START=131076 +# Trace emitted to signal that the note consuming has ended. +const.NOTE_EXECUTION_END=131077 + +# Trace emitted to signal that the transaction script processing has started. +const.TX_SCRIPT_PROCESSING_START=131078 +# Trace emitted to signal that the transaction script processing has ended. +const.TX_SCRIPT_PROCESSING_END=131079 + +# Trace emitted to signal that an execution of the transaction epilogue has started. +const.EPILOGUE_START=131080 +# Trace emitted to signal that an execution of the transaction epilogue has ended. +const.EPILOGUE_END=131081 + +# MAIN +# ================================================================================================= + #! This is the entrypoint for the transaction kernel program. It is composed of the following #! program sections: #! @@ -63,13 +94,31 @@ proc.main.1 # Prologue # --------------------------------------------------------------------------------------------- + # TODO: we execute `push.0 drop` before `trace` as decorators are not supported without other + # instructions - see: https://github.com/0xPolygonMiden/miden-vm/issues/1122 + # emit trace to signal that the execution of the prologue has started + push.0 drop + trace.PROLOGUE_START + # execute the transaction prologue exec.prologue::prepare_transaction # => [] + # TODO: we execute `push.0 drop` before `trace` as decorators are not supported without other + # instructions - see: https://github.com/0xPolygonMiden/miden-vm/issues/1122 + # emit trace to signal that the execution of the prologue has ended + push.0 drop + trace.PROLOGUE_END + # Note Processing # --------------------------------------------------------------------------------------------- + # TODO: we execute `push.0 drop` before `trace` as decorators are not supported without other + # instructions - see: https://github.com/0xPolygonMiden/miden-vm/issues/1122 + # emit trace to signal that the notes processing has started + push.0 drop + trace.NOTES_PROCESSING_START + # get the total number of consumed notes exec.memory::get_total_num_consumed_notes # => [num_consumed_notes] @@ -85,6 +134,12 @@ proc.main.1 # loop while we have notes to consume while.true + # TODO: we execute `push.0 drop` before `trace` as decorators are not supported without other + # instructions - see: https://github.com/0xPolygonMiden/miden-vm/issues/1122 + # emit trace to signal that the note execution has started + push.0 drop + trace.NOTE_EXECUTION_START + # execute the note setup script exec.note::prepare_note # => [NOTE_SCRIPT_HASH, NOTE_ARGS] @@ -102,15 +157,33 @@ proc.main.1 loc_load.0 neq # => [should_loop] + + # TODO: we execute `push.0 drop` before `trace` as decorators are not supported without other + # instructions - see: https://github.com/0xPolygonMiden/miden-vm/issues/1122 + # emit trace to signal that the note execution has ended + push.0 drop + trace.NOTE_EXECUTION_END end # execute note processing teardown exec.note::note_processing_teardown # => [] + # TODO: we execute `push.0 drop` before `trace` as decorators are not supported without other + # instructions - see: https://github.com/0xPolygonMiden/miden-vm/issues/1122 + # emit trace to signal that the notes processing has ended + push.0 drop + trace.NOTES_PROCESSING_END + # Transaction Script Processing # --------------------------------------------------------------------------------------------- + # TODO: we execute `push.0 drop` before `trace` as decorators are not supported without other + # instructions - see: https://github.com/0xPolygonMiden/miden-vm/issues/1122 + # emit trace to signal that the processing of the transaction script has started + push.0 drop + trace.TX_SCRIPT_PROCESSING_START + # execute the transaction script exec.memory::get_tx_script_root # => [TX_SCRIPT_ROOT] @@ -132,12 +205,30 @@ proc.main.1 # => [] end + # TODO: we execute `push.0 drop` before `trace` as decorators are not supported without other + # instructions - see: https://github.com/0xPolygonMiden/miden-vm/issues/1122 + # emit trace to signal that the processing of the transaction script has ended + push.0 drop + trace.TX_SCRIPT_PROCESSING_END + # Epilogue # --------------------------------------------------------------------------------------------- + # TODO: we execute `push.0 drop` before `trace` as decorators are not supported without other + # instructions - see: https://github.com/0xPolygonMiden/miden-vm/issues/1122 + # emit trace to signal that the execution of the epilogue has started + push.0 drop + trace.EPILOGUE_START + # execute the transaction epilogue exec.epilogue::finalize_transaction # => [CREATED_NOTES_COMMITMENT, FINAL_ACCOUNT_HASH] + + # TODO: we execute `push.0 drop` before `trace` as decorators are not supported without other + # instructions - see: https://github.com/0xPolygonMiden/miden-vm/issues/1122 + # emit trace to signal that the execution of the epilogue has ended + push.0 drop + trace.EPILOGUE_END end begin diff --git a/miden-lib/src/transaction/errors.rs b/miden-lib/src/transaction/errors.rs index 5a7b10fe0..3869168f3 100644 --- a/miden-lib/src/transaction/errors.rs +++ b/miden-lib/src/transaction/errors.rs @@ -105,3 +105,28 @@ impl fmt::Display for TransactionEventParsingError { #[cfg(feature = "std")] impl std::error::Error for TransactionEventParsingError {} + +// TRANSACTION TRACE PARSING ERROR +// ================================================================================================ + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum TransactionTraceParsingError { + InvalidTransactionTrace(u32), + NotTransactionTrace(u32), +} + +impl fmt::Display for TransactionTraceParsingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidTransactionTrace(trace_id) => { + write!(f, "trace {trace_id} is invalid") + }, + Self::NotTransactionTrace(trace_id) => { + write!(f, "trace {trace_id} is not a transaction kernel trace") + }, + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for TransactionTraceParsingError {} diff --git a/miden-lib/src/transaction/events.rs b/miden-lib/src/transaction/events.rs index 639549eac..d409800be 100644 --- a/miden-lib/src/transaction/events.rs +++ b/miden-lib/src/transaction/events.rs @@ -1,6 +1,12 @@ use core::fmt; -use super::TransactionEventParsingError; +use super::{TransactionEventParsingError, TransactionTraceParsingError}; + +// CONSTANTS +// ================================================================================================ + +/// Value of the top 16 bits of a transaction kernel event ID. +pub const EVENT_ID_PREFIX: u32 = 2; // TRANSACTION EVENT // ================================================================================================ @@ -32,11 +38,6 @@ pub enum TransactionEvent { AccountStorageSetMapItem = ACCOUNT_STORAGE_SET_MAP_ITEM, } -impl TransactionEvent { - /// Value of the top 16 bits of a transaction kernel event ID. - pub const EVENT_ID_PREFIX: u16 = 2; -} - impl fmt::Display for TransactionEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") @@ -47,7 +48,7 @@ impl TryFrom for TransactionEvent { type Error = TransactionEventParsingError; fn try_from(value: u32) -> Result { - if value >> 16 != Self::EVENT_ID_PREFIX as u32 { + if value >> 16 != EVENT_ID_PREFIX { return Err(TransactionEventParsingError::NotTransactionEvent(value)); } @@ -63,3 +64,51 @@ impl TryFrom for TransactionEvent { } } } + +// TRANSACTION TRACE +// ================================================================================================ + +#[repr(u32)] +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum TransactionTrace { + PrologueStart = 0x2_0000, // 131072 + PrologueEnd = 0x2_0001, // 131073 + NotesProcessingStart = 0x2_0002, // 131074 + NotesProcessingEnd = 0x2_0003, // 131075 + NoteExecutionStart = 0x2_0004, // 131076 + NoteExecutionEnd = 0x2_0005, // 131077 + TxScriptProcessingStart = 0x2_0006, // 131078 + TxScriptProcessingEnd = 0x2_0007, // 131079 + EpilogueStart = 0x2_0008, // 131080 + EpilogueEnd = 0x2_0009, // 131081 +} + +impl fmt::Display for TransactionTrace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl TryFrom for TransactionTrace { + type Error = TransactionTraceParsingError; + + fn try_from(value: u32) -> Result { + if value >> 16 != EVENT_ID_PREFIX { + return Err(TransactionTraceParsingError::NotTransactionTrace(value)); + } + + match value { + 0x2_0000 => Ok(TransactionTrace::PrologueStart), + 0x2_0001 => Ok(TransactionTrace::PrologueEnd), + 0x2_0002 => Ok(TransactionTrace::NotesProcessingStart), + 0x2_0003 => Ok(TransactionTrace::NotesProcessingEnd), + 0x2_0004 => Ok(TransactionTrace::NoteExecutionStart), + 0x2_0005 => Ok(TransactionTrace::NoteExecutionEnd), + 0x2_0006 => Ok(TransactionTrace::TxScriptProcessingStart), + 0x2_0007 => Ok(TransactionTrace::TxScriptProcessingEnd), + 0x2_0008 => Ok(TransactionTrace::EpilogueStart), + 0x2_0009 => Ok(TransactionTrace::EpilogueEnd), + _ => Err(TransactionTraceParsingError::InvalidTransactionTrace(value)), + } + } +} diff --git a/miden-lib/src/transaction/mod.rs b/miden-lib/src/transaction/mod.rs index bfaa4f4cc..1e734e5c1 100644 --- a/miden-lib/src/transaction/mod.rs +++ b/miden-lib/src/transaction/mod.rs @@ -15,7 +15,7 @@ use super::MidenLib; pub mod memory; mod events; -pub use events::TransactionEvent; +pub use events::{TransactionEvent, TransactionTrace}; mod inputs; pub use inputs::ToTransactionKernelInputs; @@ -26,7 +26,9 @@ pub use outputs::{ }; mod errors; -pub use errors::{TransactionEventParsingError, TransactionKernelError}; +pub use errors::{ + TransactionEventParsingError, TransactionKernelError, TransactionTraceParsingError, +}; // TRANSACTION KERNEL // ================================================================================================ diff --git a/miden-tx/src/executor/mod.rs b/miden-tx/src/executor/mod.rs index 38a5f6821..4fa6f6069 100644 --- a/miden-tx/src/executor/mod.rs +++ b/miden-tx/src/executor/mod.rs @@ -43,6 +43,7 @@ pub struct TransactionExecutor { impl TransactionExecutor { // CONSTRUCTOR // -------------------------------------------------------------------------------------------- + /// Creates a new [TransactionExecutor] instance with the specified [DataStore]. pub fn new(data_store: D) -> Self { Self { @@ -76,6 +77,16 @@ impl TransactionExecutor { self } + /// Enables tracing for the created instance of [TransactionExecutor]. + /// + /// When tracing is enabled, the executor will receive tracing events as various stages of the + /// transaction kernel complete. This enables collecting basic stats about how long different + /// stages of transaction execution take. + pub fn with_tracing(mut self) -> Self { + self.exec_options = self.exec_options.with_tracing(); + self + } + // STATE MUTATORS // -------------------------------------------------------------------------------------------- @@ -203,7 +214,7 @@ impl TransactionExecutor { /// Returns an error if: /// - If required data can not be fetched from the [DataStore]. /// - If the transaction can not be compiled. - fn prepare_transaction( + pub fn prepare_transaction( &self, account_id: AccountId, block_ref: u32, diff --git a/miden-tx/src/host/mod.rs b/miden-tx/src/host/mod.rs index c662f46fd..0d18fc5aa 100644 --- a/miden-tx/src/host/mod.rs +++ b/miden-tx/src/host/mod.rs @@ -1,7 +1,8 @@ use alloc::{collections::BTreeMap, string::ToString, vec::Vec}; use miden_lib::transaction::{ - memory::ACCT_STORAGE_ROOT_PTR, TransactionEvent, TransactionKernelError, + memory::{ACCT_STORAGE_ROOT_PTR, CURRENT_CONSUMED_NOTE_PTR}, + TransactionEvent, TransactionKernelError, TransactionTrace, }; use miden_objects::{ accounts::{AccountDelta, AccountId, AccountStorage, AccountStub}, @@ -24,6 +25,9 @@ use account_delta_tracker::AccountDeltaTracker; mod account_procs; use account_procs::AccountProcedureIndexMap; +mod tx_progress; +pub use tx_progress::TransactionProgress; + // CONSTANTS // ================================================================================================ @@ -46,6 +50,10 @@ pub struct TransactionHost { /// The list of notes created while executing a transaction. output_notes: Vec, + + /// Contains the information about the number of cycles for each of the transaction execution + /// stages. + tx_progress: TransactionProgress, } impl TransactionHost { @@ -57,6 +65,7 @@ impl TransactionHost { account_delta: AccountDeltaTracker::new(&account), acct_procedure_index_map: proc_index_map, output_notes: Vec::new(), + tx_progress: TransactionProgress::default(), } } @@ -65,6 +74,11 @@ impl TransactionHost { (self.adv_provider, self.account_delta.into_delta(), self.output_notes) } + /// Returns a reference to the `tx_progress` field of the [`TransactionHost`]. + pub fn tx_progress(&self) -> &TransactionProgress { + &self.tx_progress + } + // EVENT HANDLERS // -------------------------------------------------------------------------------------------- @@ -261,6 +275,36 @@ impl TransactionHost { self.account_delta.vault_tracker().remove_asset(asset); Ok(()) } + + // HELPER FUNCTIONS + // -------------------------------------------------------------------------------------------- + + /// Returns the ID of the currently executing input note, or None if the note execution hasn't + /// started yet or has already ended. + /// + /// # Errors + /// Returns an error if the address of the currently executing input note is invalid (e.g., + /// greater than `u32::MAX`). + fn get_current_note_id(process: &S) -> Result, ExecutionError> { + // get the word where note address is stored + let note_address_word = process.get_mem_value(process.ctx(), CURRENT_CONSUMED_NOTE_PTR); + // get the note address in `Felt` from or return `None` if the address hasn't been accessed + // previously. + let note_address_felt = match note_address_word { + Some(w) => w[0], + None => return Ok(None), + }; + // get the note address + let note_address: u32 = note_address_felt + .try_into() + .map_err(|_| ExecutionError::MemoryAddressOutOfBounds(note_address_felt.as_int()))?; + // if `note_address` == 0 note execution has ended and there is no valid note address + if note_address == 0 { + Ok(None) + } else { + Ok(process.get_mem_value(process.ctx(), note_address).map(NoteId::from)) + } + } } impl Host for TransactionHost { @@ -313,4 +357,33 @@ impl Host for TransactionHost { Ok(HostResponse::None) } + + fn on_trace( + &mut self, + process: &S, + trace_id: u32, + ) -> Result { + let event = TransactionTrace::try_from(trace_id) + .map_err(|err| ExecutionError::EventError(err.to_string()))?; + + use TransactionTrace::*; + match event { + PrologueStart => self.tx_progress.start_prologue(process.clk()), + PrologueEnd => self.tx_progress.end_prologue(process.clk()), + NotesProcessingStart => self.tx_progress.start_notes_processing(process.clk()), + NotesProcessingEnd => self.tx_progress.end_notes_processing(process.clk()), + NoteExecutionStart => { + let note_id = Self::get_current_note_id(process)? + .expect("Note execution interval measurement is incorrect: check the placement of the start and the end of the interval"); + self.tx_progress.start_note_execution(process.clk(), note_id); + }, + NoteExecutionEnd => self.tx_progress.end_note_execution(process.clk()), + TxScriptProcessingStart => self.tx_progress.start_tx_script_processing(process.clk()), + TxScriptProcessingEnd => self.tx_progress.end_tx_script_processing(process.clk()), + EpilogueStart => self.tx_progress.start_epilogue(process.clk()), + EpilogueEnd => self.tx_progress.end_epilogue(process.clk()), + } + + Ok(HostResponse::None) + } } diff --git a/miden-tx/src/host/tx_progress.rs b/miden-tx/src/host/tx_progress.rs new file mode 100644 index 000000000..3e4cab823 --- /dev/null +++ b/miden-tx/src/host/tx_progress.rs @@ -0,0 +1,120 @@ +pub use alloc::vec::Vec; + +use miden_objects::notes::NoteId; + +// TRANSACTION PROGRESS +// ================================================================================================ + +/// Contains the information about the number of cycles for each of the transaction execution +/// stages. +#[derive(Clone, Default)] +pub struct TransactionProgress { + prologue: CycleInterval, + notes_processing: CycleInterval, + note_execution: Vec<(NoteId, CycleInterval)>, + tx_script_processing: CycleInterval, + epilogue: CycleInterval, +} + +impl TransactionProgress { + // STATE ACCESSORS + // -------------------------------------------------------------------------------------------- + + pub fn prologue(&self) -> &CycleInterval { + &self.prologue + } + + pub fn notes_processing(&self) -> &CycleInterval { + &self.notes_processing + } + + pub fn note_execution(&self) -> &Vec<(NoteId, CycleInterval)> { + &self.note_execution + } + + pub fn tx_script_processing(&self) -> &CycleInterval { + &self.tx_script_processing + } + + pub fn epilogue(&self) -> &CycleInterval { + &self.epilogue + } + + // STATE MUTATORS + // -------------------------------------------------------------------------------------------- + + pub fn start_prologue(&mut self, cycle: u32) { + self.prologue.set_start(cycle); + } + + pub fn end_prologue(&mut self, cycle: u32) { + self.prologue.set_end(cycle); + } + + pub fn start_notes_processing(&mut self, cycle: u32) { + self.notes_processing.set_start(cycle); + } + + pub fn end_notes_processing(&mut self, cycle: u32) { + self.notes_processing.set_end(cycle); + } + + pub fn start_note_execution(&mut self, cycle: u32, note_id: NoteId) { + self.note_execution.push((note_id, CycleInterval::new(cycle))); + } + + pub fn end_note_execution(&mut self, cycle: u32) { + if let Some((_, interval)) = self.note_execution.last_mut() { + interval.set_end(cycle) + } + } + + pub fn start_tx_script_processing(&mut self, cycle: u32) { + self.tx_script_processing.set_start(cycle); + } + + pub fn end_tx_script_processing(&mut self, cycle: u32) { + self.tx_script_processing.set_end(cycle); + } + + pub fn start_epilogue(&mut self, cycle: u32) { + self.epilogue.set_start(cycle); + } + + pub fn end_epilogue(&mut self, cycle: u32) { + self.epilogue.set_end(cycle); + } +} + +/// Stores the cycles corresponding to the start and the end of an interval. +#[derive(Clone, Default)] +pub struct CycleInterval { + start: Option, + end: Option, +} + +impl CycleInterval { + pub fn new(start: u32) -> Self { + Self { start: Some(start), end: None } + } + + pub fn set_start(&mut self, s: u32) { + self.start = Some(s); + } + + pub fn set_end(&mut self, e: u32) { + self.end = Some(e); + } + + /// Calculate the length of the interval + pub fn len(&self) -> Option { + if let Some(start) = self.start { + if let Some(end) = self.end { + if end >= start { + return Some(end - start); + } + } + } + None + } +} diff --git a/miden-tx/src/lib.rs b/miden-tx/src/lib.rs index 73d772e40..c461f234e 100644 --- a/miden-tx/src/lib.rs +++ b/miden-tx/src/lib.rs @@ -24,7 +24,7 @@ mod executor; pub use executor::{DataStore, TransactionExecutor}; pub mod host; -pub use host::TransactionHost; +pub use host::{TransactionHost, TransactionProgress}; mod prover; pub use prover::{ProvingOptions, TransactionProver}; diff --git a/mock/src/lib.rs b/mock/src/lib.rs index cf1d77891..6e50c1a73 100644 --- a/mock/src/lib.rs +++ b/mock/src/lib.rs @@ -25,9 +25,9 @@ use miden_objects::{ Felt, }; use mock::host::MockHost; -use vm_processor::{AdviceInputs, ExecutionError, ExecutionOptions, Process, Word}; +use vm_processor::{AdviceInputs, ExecutionError, Process, Word}; #[cfg(feature = "std")] -use vm_processor::{AdviceProvider, DefaultHost, Host, StackInputs}; +use vm_processor::{AdviceProvider, DefaultHost, ExecutionOptions, Host, StackInputs}; pub mod builders; pub mod constants;