From a3775f66ff997de88141c0ca959d849dd946c832 Mon Sep 17 00:00:00 2001 From: riemann Date: Tue, 9 Jun 2026 23:53:03 -0400 Subject: [PATCH 1/2] perf: avoid double-hashing the asset vault key in fungible add/remove Hash the raw asset vault key once at the start of each asset vault modifier and have peek_asset/get_asset accept the already-hashed key, removing one poseidon2 hash per fungible asset add/remove (including the fee-removing path in the epilogue). The set_asset helper becomes a bare smt::set wrapper and is removed. Worst-case post-compute_fee cycles drop from 863 to 843; VAULT_KEY_HASH_CYCLES is re-measured from 50 to 30 (same 45-cycle margin). Closes #3059 --- .../asm/kernels/transaction/lib/account.masm | 8 ++ .../kernels/transaction/lib/asset_vault.masm | 122 ++++++++---------- .../asm/kernels/transaction/lib/epilogue.masm | 11 +- .../src/kernel_tests/tx/test_asset_vault.rs | 4 + .../src/kernel_tests/tx/test_faucet.rs | 5 + .../src/kernel_tests/tx/test_fee.rs | 2 +- 6 files changed, 79 insertions(+), 73 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm index c88b1d3e1f..82939fd098 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm @@ -799,6 +799,10 @@ pub proc get_asset emit.ACCOUNT_VAULT_BEFORE_GET_ASSET_EVENT # => [ASSET_KEY, vault_root_ptr] + # hash the asset vault key before using it as the SMT key + exec.asset_vault::hash_asset_key + # => [ASSET_KEY_HASH, vault_root_ptr] + # get the asset exec.asset_vault::get_asset # => [ASSET_VALUE] @@ -822,6 +826,10 @@ pub proc get_initial_asset emit.ACCOUNT_VAULT_BEFORE_GET_ASSET_EVENT # => [ASSET_KEY, init_native_vault_root_ptr] + # hash the asset vault key before using it as the SMT key + exec.asset_vault::hash_asset_key + # => [ASSET_KEY_HASH, init_native_vault_root_ptr] + # get the asset exec.asset_vault::get_asset # => [ASSET_VALUE] diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/asset_vault.masm b/crates/miden-protocol/asm/kernels/transaction/lib/asset_vault.masm index 8dc5cca9fa..99a4cb0bbc 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/asset_vault.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/asset_vault.masm @@ -19,20 +19,16 @@ const ERR_VAULT_NON_FUNGIBLE_ASSET_TO_REMOVE_NOT_FOUND="failed to remove non-exi # ACCESSORS # ================================================================================================= -#! Returns the ASSET_VALUE associated with the provided asset vault key. +#! Returns the ASSET_VALUE associated with the provided hashed asset vault key. #! -#! Inputs: [ASSET_KEY, vault_root_ptr] +#! Inputs: [ASSET_KEY_HASH, vault_root_ptr] #! Outputs: [ASSET_VALUE] #! #! Where: #! - vault_root_ptr is a pointer to the memory location at which the vault root is stored. -#! - ASSET_KEY is the asset vault key of the asset to fetch. +#! - ASSET_KEY_HASH is the hashed asset vault key of the asset to fetch (see hash_asset_key). #! - ASSET_VALUE is the value of the asset from the vault, which can be the EMPTY_WORD if it isn't present. pub proc get_asset - # hash the asset vault key before using it as the SMT key - exec.hash_asset_key - # => [ASSET_KEY_HASH, vault_root_ptr] - # load the asset vault root from memory padw movup.8 mem_loadw_le # => [ASSET_VAULT_ROOT, ASSET_KEY_HASH] @@ -45,7 +41,7 @@ pub proc get_asset # => [ASSET_VALUE] end -#! Returns the _peeked_ asset associated with the provided asset vault key. +#! Returns the _peeked_ asset associated with the provided hashed asset vault key. #! #! WARNING: Peeked means the asset is loaded from the advice provider, which is susceptible to #! manipulation from a malicious host. Therefore this should only be used when the inclusion of the @@ -61,18 +57,14 @@ end #! merkle paths from the merkle store, since this is only possible for the account vault. Ensure #! that the merkle paths are present prior to calling. #! -#! Inputs: [ASSET_KEY, vault_root_ptr] +#! Inputs: [ASSET_KEY_HASH, vault_root_ptr] #! Outputs: [ASSET_VALUE] #! #! Where: #! - vault_root_ptr is a pointer to the memory location at which the vault root is stored. -#! - ASSET_KEY is the asset vault key of the asset to fetch. +#! - ASSET_KEY_HASH is the hashed asset vault key of the asset to fetch (see hash_asset_key). #! - ASSET_VALUE is the retrieved asset. pub proc peek_asset - # hash the asset vault key before using it as the SMT key - exec.hash_asset_key - # => [ASSET_KEY_HASH, vault_root_ptr] - # load the asset vault root from memory padw movup.8 mem_loadw_le # => [ASSET_VAULT_ROOT, ASSET_KEY_HASH] @@ -130,41 +122,45 @@ pub proc add_fungible_asset movup.8 loc_store.0 # => [ASSET_KEY, ASSET_VALUE] + # hash the asset vault key once; it is used as the SMT key for both the peek and the set below + exec.hash_asset_key + # => [ASSET_KEY_HASH, ASSET_VALUE] + dupw loc_load.0 movdn.4 - # => [ASSET_KEY, vault_root_ptr, ASSET_KEY, ASSET_VALUE] + # => [ASSET_KEY_HASH, vault_root_ptr, ASSET_KEY_HASH, ASSET_VALUE] exec.peek_asset - # => [INITIAL_ASSET_VALUE, ASSET_KEY, ASSET_VALUE] + # => [INITIAL_ASSET_VALUE, ASSET_KEY_HASH, ASSET_VALUE] # since we have peeked the value, we need to later assert that the actual value matches this # one, so we'll keep a copy for later swapw dupw.1 - # => [INITIAL_ASSET_VALUE, ASSET_KEY, INITIAL_ASSET_VALUE, ASSET_VALUE] + # => [INITIAL_ASSET_VALUE, ASSET_KEY_HASH, INITIAL_ASSET_VALUE, ASSET_VALUE] movupw.3 - # => [ASSET_VALUE, INITIAL_ASSET_VALUE, ASSET_KEY, INITIAL_ASSET_VALUE] + # => [ASSET_VALUE, INITIAL_ASSET_VALUE, ASSET_KEY_HASH, INITIAL_ASSET_VALUE] # Merge the assets. # --------------------------------------------------------------------------------------------- exec.fungible_asset::merge - # => [FINAL_ASSET_VALUE, ASSET_KEY, INITIAL_ASSET_VALUE] + # => [FINAL_ASSET_VALUE, ASSET_KEY_HASH, INITIAL_ASSET_VALUE] swapw - # => [ASSET_KEY, FINAL_ASSET_VALUE, INITIAL_ASSET_VALUE] + # => [ASSET_KEY_HASH, FINAL_ASSET_VALUE, INITIAL_ASSET_VALUE] # Insert the merged asset. # --------------------------------------------------------------------------------------------- # load the vault root padw loc_load.0 mem_loadw_le - # => [VAULT_ROOT, ASSET_KEY, FINAL_ASSET_VALUE, INITIAL_ASSET_VALUE] + # => [VAULT_ROOT, ASSET_KEY_HASH, FINAL_ASSET_VALUE, INITIAL_ASSET_VALUE] swapw dupw.2 - # => [FINAL_ASSET_VALUE, ASSET_KEY, VAULT_ROOT, FINAL_ASSET_VALUE, INITIAL_ASSET_VALUE] + # => [FINAL_ASSET_VALUE, ASSET_KEY_HASH, VAULT_ROOT, FINAL_ASSET_VALUE, INITIAL_ASSET_VALUE] - # hash the asset key and update the asset in the vault - exec.set_asset + # update the asset in the vault + exec.smt::set # => [PREV_VAULT_VALUE, NEW_VAULT_ROOT, FINAL_ASSET_VALUE, INITIAL_ASSET_VALUE] # assert PREV_VAULT_VALUE = INITIAL_ASSET_VALUE to make sure peek_asset returned the correct asset @@ -194,20 +190,24 @@ end #! Panics if: #! - the vault already contains the same non-fungible asset. pub proc add_non_fungible_asset + # hash the asset vault key before using it as the SMT key + exec.hash_asset_key + # => [ASSET_KEY_HASH, ASSET_VALUE, vault_root_ptr] + # Load VAULT_ROOT and insert asset. # --------------------------------------------------------------------------------------------- padw dup.12 - # => [vault_root_ptr, pad(4), ASSET_KEY, ASSET_VALUE, vault_root_ptr] + # => [vault_root_ptr, pad(4), ASSET_KEY_HASH, ASSET_VALUE, vault_root_ptr] mem_loadw_le swapw - # => [ASSET_KEY, VAULT_ROOT, ASSET_VALUE, vault_root_ptr] + # => [ASSET_KEY_HASH, VAULT_ROOT, ASSET_VALUE, vault_root_ptr] dupw.2 - # => [ASSET_VALUE, ASSET_KEY, VAULT_ROOT, ASSET_VALUE, vault_root_ptr] + # => [ASSET_VALUE, ASSET_KEY_HASH, VAULT_ROOT, ASSET_VALUE, vault_root_ptr] - # hash the asset key and insert the asset into the vault - exec.set_asset + # insert the asset into the vault + exec.smt::set # => [OLD_VAL, VAULT_ROOT', ASSET_VALUE, vault_root_ptr] # assert old value was empty @@ -296,38 +296,42 @@ end #! - the amount of the asset in the vault is less than the amount to be removed. @locals(4) pub proc remove_fungible_asset + # hash the asset vault key once; it is used as the SMT key for both the peek and the set below + exec.hash_asset_key + # => [ASSET_KEY_HASH, ASSET_VALUE, vault_root_ptr] + dupw movdnw.2 - # => [ASSET_KEY, ASSET_VALUE, ASSET_KEY, vault_root_ptr] + # => [ASSET_KEY_HASH, ASSET_VALUE, ASSET_KEY_HASH, vault_root_ptr] dup.12 movdn.4 - # => [ASSET_KEY, vault_root_ptr, ASSET_VALUE, ASSET_KEY, vault_root_ptr] + # => [ASSET_KEY_HASH, vault_root_ptr, ASSET_VALUE, ASSET_KEY_HASH, vault_root_ptr] exec.peek_asset - # => [INITIAL_ASSET_VALUE, ASSET_VALUE, ASSET_KEY, vault_root_ptr] + # => [INITIAL_ASSET_VALUE, ASSET_VALUE, ASSET_KEY_HASH, vault_root_ptr] movdnw.2 - # => [ASSET_VALUE, ASSET_KEY, INITIAL_ASSET_VALUE, vault_root_ptr] + # => [ASSET_VALUE, ASSET_KEY_HASH, INITIAL_ASSET_VALUE, vault_root_ptr] dupw.2 swapw - # => [ASSET_VALUE, INITIAL_ASSET_VALUE, ASSET_KEY, INITIAL_ASSET_VALUE, vault_root_ptr] + # => [ASSET_VALUE, INITIAL_ASSET_VALUE, ASSET_KEY_HASH, INITIAL_ASSET_VALUE, vault_root_ptr] # compute FINAL_ASSET_VALUE = INITIAL_ASSET_VALUE - ASSET_VALUE exec.fungible_asset::split - # => [FINAL_ASSET_VALUE, ASSET_KEY, INITIAL_ASSET_VALUE, vault_root_ptr] + # => [FINAL_ASSET_VALUE, ASSET_KEY_HASH, INITIAL_ASSET_VALUE, vault_root_ptr] # store FINAL_ASSET_VALUE so we can return it at the end loc_storew_le.0 - # => [FINAL_ASSET_VALUE, ASSET_KEY, INITIAL_ASSET_VALUE, vault_root_ptr] + # => [FINAL_ASSET_VALUE, ASSET_KEY_HASH, INITIAL_ASSET_VALUE, vault_root_ptr] dup.12 padw movup.4 mem_loadw_le - # => [VAULT_ROOT, FINAL_ASSET_VALUE, ASSET_KEY, INITIAL_ASSET_VALUE, vault_root_ptr] + # => [VAULT_ROOT, FINAL_ASSET_VALUE, ASSET_KEY_HASH, INITIAL_ASSET_VALUE, vault_root_ptr] movdnw.2 - # => [FINAL_ASSET_VALUE, ASSET_KEY, VAULT_ROOT, INITIAL_ASSET_VALUE, vault_root_ptr] + # => [FINAL_ASSET_VALUE, ASSET_KEY_HASH, VAULT_ROOT, INITIAL_ASSET_VALUE, vault_root_ptr] - # hash the asset key and update the asset in the vault; the old value is asserted below to be - # equivalent to the peeked value provided via peek_asset - exec.set_asset + # update the asset in the vault; the old value is asserted below to be equivalent to the + # peeked value provided via peek_asset + exec.smt::set # => [OLD_VALUE, NEW_VAULT_ROOT, INITIAL_ASSET_VALUE, vault_root_ptr] dupw.2 @@ -371,16 +375,20 @@ end #! Panics if: #! - the non-fungible asset is not found in the vault. pub proc remove_non_fungible_asset + # hash the asset vault key before using it as the SMT key + exec.hash_asset_key + # => [ASSET_KEY_HASH, ASSET_VALUE, vault_root_ptr] + # load vault root padw dup.12 mem_loadw_le - # => [VAULT_ROOT, ASSET_KEY, ASSET_VALUE, vault_root_ptr] + # => [VAULT_ROOT, ASSET_KEY_HASH, ASSET_VALUE, vault_root_ptr] - # prepare insertion of an EMPTY_WORD into the vault at the asset key to remove the asset + # prepare insertion of an EMPTY_WORD into the vault at the hashed asset key to remove the asset swapw padw - # => [EMPTY_WORD, ASSET_KEY, VAULT_ROOT, ASSET_VALUE, vault_root_ptr] + # => [EMPTY_WORD, ASSET_KEY_HASH, VAULT_ROOT, ASSET_VALUE, vault_root_ptr] - # hash the asset key and insert the empty word into the vault to remove the asset - exec.set_asset + # insert the empty word into the vault to remove the asset + exec.smt::set # => [REMOVED_ASSET_VALUE, NEW_VAULT_ROOT, ASSET_VALUE, vault_root_ptr] # dup ASSET_VALUE so it survives the assert; the assert proves it equals REMOVED_ASSET_VALUE @@ -447,27 +455,7 @@ end #! #! Inputs: [ASSET_KEY] #! Outputs: [ASSET_KEY_HASH] -proc hash_asset_key +pub proc hash_asset_key exec.poseidon2::hash # => [ASSET_KEY_HASH] end - -#! Hashes the raw asset vault key and writes ASSET_VALUE into the asset vault SMT at the hashed key, -#! returning the previous value stored there. -#! -#! Inputs: [ASSET_VALUE, ASSET_KEY, VAULT_ROOT] -#! Outputs: [OLD_VALUE, NEW_VAULT_ROOT] -#! -#! Where: -#! - ASSET_KEY is the raw (unhashed) asset vault key. -#! - ASSET_VALUE is the value to write into the vault at the hashed key. -#! - VAULT_ROOT is the current root of the asset vault SMT. -#! - OLD_VALUE is the value previously stored at the hashed key. -#! - NEW_VAULT_ROOT is the root of the asset vault SMT after the write. -proc set_asset - swapw exec.hash_asset_key swapw - # => [ASSET_VALUE, ASSET_KEY_HASH, VAULT_ROOT] - - exec.smt::set - # => [OLD_VALUE, NEW_VAULT_ROOT] -end diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm index 3fc5363b4b..eaedf0a4c4 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm @@ -46,12 +46,13 @@ const EPILOGUE_AUTH_PROC_END_EVENT=event("miden::protocol::epilogue::auth_proc_e # number of cycles, and so we only need to add the difference. const SMT_SET_ADDITIONAL_CYCLES=250 -# An upper-bound estimate of the cycles needed to hash the asset vault key before the fee-removing -# smt::set. This cost is incurred only when a fee is actually removed from the asset vault, and is -# kept separate from SMT_SET_ADDITIONAL_CYCLES (which models smt::set's own best/worst-case spread). +# An upper-bound estimate of the cycles needed to hash the asset vault key inside the fee-removing +# remove_fungible_asset (the key is hashed once, at the start of the procedure). This cost is +# incurred only when a fee is actually removed from the asset vault, and is kept separate from +# SMT_SET_ADDITIONAL_CYCLES (which models smt::set's own best/worst-case spread). # It is additive rather than double-counted: the lowest-observed NUM_POST_COMPUTE_FEE_CYCLES below # comes from a zero-fee transaction that skips fee removal entirely, and so excludes this hashing. -const VAULT_KEY_HASH_CYCLES=50 +const VAULT_KEY_HASH_CYCLES=30 # The number of cycles the epilogue is estimated to take after compute_fee has been executed, # including an unknown cycle number of the above-mentioned call to smt::set. It is safe to assume @@ -61,7 +62,7 @@ const VAULT_KEY_HASH_CYCLES=50 const NUM_POST_COMPUTE_FEE_CYCLES=608 # Upper bound on the post-compute_fee cycle count; it must stay an upper bound or the verification -# fee is undercharged. Worst case observed is 863 cycles (45 of margin); re-measure when the +# fee is undercharged. Worst case observed is 843 cycles (45 of margin); re-measure when the # epilogue or smt::set changes. An adversary cannot inflate the fee-removal smt::set cost: vault # keys are hashed before insertion (see AssetVaultKey::hash). The multi-leaf smt::set worst case is # not yet exercised (see test_fee.rs TODO). diff --git a/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs b/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs index d0ff5ea885..917ab3d3b7 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs @@ -91,6 +91,10 @@ async fn peek_asset_returns_correct_asset() -> anyhow::Result<()> { emit.event("miden::protocol::account::vault_before_get_asset") # => [ASSET_KEY, account_vault_root_ptr] + # hash the asset vault key before using it as the SMT key + exec.asset_vault::hash_asset_key + # => [ASSET_KEY_HASH, account_vault_root_ptr] + exec.asset_vault::peek_asset # => [PEEKED_ASSET_VALUE] diff --git a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs index 30918fd0d1..dc8c079000 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs @@ -67,6 +67,7 @@ async fn test_mint_fungible_asset_succeeds() -> anyhow::Result<()> { # assert the input vault has been updated push.{INPUT_VAULT_ROOT_PTR} push.{FUNGIBLE_ASSET_KEY} + exec.asset_vault::hash_asset_key exec.asset_vault::get_asset # => [ASSET_VALUE] @@ -256,6 +257,7 @@ async fn test_mint_non_fungible_asset_succeeds() -> anyhow::Result<()> { # assert the input vault has been updated. push.{INPUT_VAULT_ROOT_PTR} push.{NON_FUNGIBLE_ASSET_KEY} + exec.asset_vault::hash_asset_key exec.asset_vault::get_asset push.{NON_FUNGIBLE_ASSET_VALUE} assert_eqw.err="vault should contain asset" @@ -406,6 +408,7 @@ async fn test_burn_fungible_asset_succeeds() -> anyhow::Result<()> { push.{INPUT_VAULT_ROOT_PTR} push.{FUNGIBLE_ASSET_KEY} + exec.asset_vault::hash_asset_key exec.asset_vault::get_asset # => [ASSET_VALUE] @@ -555,6 +558,7 @@ async fn test_burn_non_fungible_asset_succeeds() -> anyhow::Result<()> { # check that the non-fungible asset is presented in the input vault push.{INPUT_VAULT_ROOT_PTR} push.{NON_FUNGIBLE_ASSET_KEY} + exec.asset_vault::hash_asset_key exec.asset_vault::get_asset push.{NON_FUNGIBLE_ASSET_VALUE} assert_eqw.err="input vault should contain the asset" @@ -568,6 +572,7 @@ async fn test_burn_non_fungible_asset_succeeds() -> anyhow::Result<()> { # assert the input vault has been updated and does not have the burnt asset push.{INPUT_VAULT_ROOT_PTR} push.{NON_FUNGIBLE_ASSET_KEY} + exec.asset_vault::hash_asset_key exec.asset_vault::get_asset # the returned word should be empty, indicating the asset is absent padw assert_eqw.err="input vault should not contain burned asset" diff --git a/crates/miden-testing/src/kernel_tests/tx/test_fee.rs b/crates/miden-testing/src/kernel_tests/tx/test_fee.rs index 85aefbbf81..28b5dfef45 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_fee.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_fee.rs @@ -97,7 +97,7 @@ async fn num_tx_cycles_after_compute_fee_are_less_than_estimated( // These constants should always be updated together with the equivalent constants in // epilogue.masm. const SMT_SET_ADDITIONAL_CYCLES: usize = 250; - const VAULT_KEY_HASH_CYCLES: usize = 50; + const VAULT_KEY_HASH_CYCLES: usize = 30; const NUM_POST_COMPUTE_FEE_CYCLES: usize = 608; assert!( From e6f0e07467d7eb613fa56b43583ded7ee6cf5883 Mon Sep 17 00:00:00 2001 From: riemann Date: Wed, 10 Jun 2026 00:00:54 -0400 Subject: [PATCH 2/2] chore: add changelog entry for #3073 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fe4a8a3c3..f3a0765ed7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Added `AccountComponent::has_procedure(root)` helper ([#2974](https://github.com/0xMiden/protocol/pull/2974)). - Optimized protocol MASM stack-cleaning sequences, saving 1 cycle per occurrence across 9 single-element-extraction procedures ([#3041](https://github.com/0xMiden/protocol/pull/3041)). - [BREAKING] Refactored `TokenPolicyManager` by adding `invoke_send_policy` / `invoke_receive_policy` wrappers (stored in the protocol reserved asset callback slots) that read the active policy root from the new `active_send_policy_proc_root` / `active_receive_policy_proc_root` storage slots ([#3047](https://github.com/0xMiden/protocol/pull/3047)). +- [BREAKING] Changed `asset_vault::get_asset` and `asset_vault::peek_asset` to accept a pre-hashed `ASSET_KEY_HASH` instead of a raw `ASSET_KEY`, and made `asset_vault::hash_asset_key` public; fungible add/remove now hash the vault key once, eliminating a redundant `poseidon2::hash` per operation ([#3073](https://github.com/0xMiden/protocol/pull/3073)). - Added a definition of the Miden operator on the architecture overview page and linked it from the note lifecycle ([#3017](https://github.com/0xMiden/protocol/pull/3017)). - Clarified Miden's operational roles on the architecture overview page and linked them from the note lifecycle ([#3017](https://github.com/0xMiden/protocol/pull/3017)).