From 7878300f0acb59a20feb555c95ece966e3008478 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Wed, 19 Aug 2026 15:28:49 +0000 Subject: [PATCH 1/7] fix(protocol): verify input-note storage item count in the prologue --- CHANGELOG.md | 1 + .../transaction-core/src/prologue.masm | 54 +++++++++++++++++-- .../src/kernel_tests/tx/test_prologue.rs | 54 +++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa88b95bc..5d0d1b8e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Fixed `PrivateOutputNote` construction and deserialization accepting attachment data that is not committed by the note header ([#3556](https://github.com/0xMiden/protocol/issues/3556)). - Fixed the authentication procedure not ending up at index 0 of an account's code when its MAST root was already exported by another component ([#3566](https://github.com/0xMiden/protocol/pull/3566)). - [BREAKING] Foreign procedure invocation now requires the provided procedure root to be part of the foreign account's code, so a caller can no longer execute arbitrary code under a foreign account's identity ([#3575](https://github.com/0xMiden/protocol/pull/3575)). +- The transaction kernel prologue now verifies each input note's storage-item count and preimage against its authenticated storage commitment ([#3593](https://github.com/0xMiden/protocol/issues/3593)). ## v0.16.0 (2026-08-17) diff --git a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm index da09786d95..2611ed152d 100644 --- a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm +++ b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm @@ -5,6 +5,7 @@ use miden::core::word use miden::tx_kernel_core::account use miden::protocol_utils::account_id +use miden::protocol_utils::mem as mem_utils use miden::tx_kernel_core::asset_vault use miden::tx_kernel_core::asset use {ASSET_SIZE} from miden::tx_kernel_core::asset @@ -62,6 +63,12 @@ const ERR_PROLOGUE_NEW_ACCOUNT_NONCE_MUST_BE_ZERO = "new account must have a zer const ERR_PROLOGUE_NUMBER_OF_NOTE_STORAGE_ITEMS_EXCEEDED_LIMIT = "number of note storage items exceeded the maximum limit of 1024" +const ERR_PROLOGUE_NOTE_STORAGE_ITEMS_COUNT_MISMATCH = + "the specified number of note storage items does not match its commitment" + +const ERR_PROLOGUE_NOTE_STORAGE_DOES_NOT_MATCH_COMMITMENT = + "note storage does not match the commitment" + const ERR_PROLOGUE_NOTE_AUTHENTICATION_FAILED = "failed to authenticate note inclusion in block" const ERR_PROLOGUE_KERNEL_PROCEDURE_COMMITMENT_MISMATCH = @@ -631,11 +638,15 @@ proc compute_note_metadata_commitment # => [NOTE_METADATA_COMMITMENT] end -#! Checks that the number of note storage is within limit and stores it to memory. +#! Checks that the number of note storage items is within limit, stores it to memory, and verifies +#! the note's storage against its authenticated storage commitment. #! #! Inputs: #! Operand stack: [note_ptr] #! Advice stack: [num_storage_items] +#! Advice map: { +#! NOTE_STORAGE_COMMITMENT: [[STORAGE_ITEMS]] +#! } #! Outputs: #! Operand stack: [note_ptr] #! Advice stack: [] @@ -643,6 +654,17 @@ end #! Where: #! - note_ptr is the memory location for the input note. #! - num_storage_items is the note's number of storage items. +#! - NOTE_STORAGE_COMMITMENT is the note's storage commitment, already stored in note memory. +#! - STORAGE_ITEMS are the note's storage items. +#! +#! Locals: +#! - 0..1024: scratch buffer for the storage preimage (MAX_NOTE_STORAGE_ITEMS elements) +#! +#! Panics if: +#! - the number of storage items exceeds the maximum limit. +#! - the advised number of storage items does not match the note's storage commitment. +#! - the note's storage does not match its storage commitment. +@locals(1024) proc process_note_num_storage_items # move the number of storage items from the advice stack to the operand stack adv_push @@ -653,8 +675,34 @@ proc process_note_num_storage_items assert.err=ERR_PROLOGUE_NUMBER_OF_NOTE_STORAGE_ITEMS_EXCEEDED_LIMIT # => [num_storage_items, note_ptr] - # store the number of storage items into the memory - dup.1 exec.memory::set_input_note_num_storage_items + # store the number of storage items into the memory, keeping the count on the stack + dup dup.2 exec.memory::set_input_note_num_storage_items + # => [num_storage_items, note_ptr] + + # load the note's authenticated storage commitment and a scratch pointer for the preimage + dup.1 exec.memory::get_input_note_storage_commitment + # => [NOTE_STORAGE_COMMITMENT, num_storage_items, note_ptr] + + locaddr.0 movdn.5 + # => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr, note_ptr] + + # load the storage preimage from the advice map, padded to the next multiple of 8 so it can be + # written with `pipe_elements_preimage_to_memory` + adv.push_mapvaln.8 + # OS => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr, note_ptr] + # AS => [advice_num_storage_items, [STORAGE_ITEMS]] + + # assert the advised item count matches the note's declared count + adv_push dup.5 assert_eq.err=ERR_PROLOGUE_NOTE_STORAGE_ITEMS_COUNT_MISMATCH + # => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr, note_ptr] + + # write the preimage to scratch memory and compute its commitment + movup.5 movup.5 + exec.mem_utils::pipe_elements_preimage_to_memory + # => [COMPUTED_COMMITMENT, NOTE_STORAGE_COMMITMENT, note_ptr] + + # assert the note's storage matches its authenticated commitment + assert_eqw.err=ERR_PROLOGUE_NOTE_STORAGE_DOES_NOT_MATCH_COMMITMENT # => [note_ptr] end diff --git a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs index f999eba7d1..30ea07885c 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs @@ -17,6 +17,7 @@ use miden_protocol::asset::{FungibleAsset, NonFungibleAsset}; use miden_protocol::block::account_tree::AccountIdKey; use miden_protocol::errors::tx_kernel::{ ERR_ACCOUNT_SEED_AND_COMMITMENT_DIGEST_MISMATCH, + ERR_PROLOGUE_NOTE_STORAGE_ITEMS_COUNT_MISMATCH, ERR_PROLOGUE_NUMBER_OF_NOTE_ASSETS_EXCEEDS_LIMIT, }; use miden_protocol::note::NoteId; @@ -202,6 +203,59 @@ async fn test_transaction_prologue_rejects_too_many_note_assets() -> anyhow::Res Ok(()) } +#[tokio::test] +async fn test_transaction_prologue_verifies_note_storage_against_commitment() -> anyhow::Result<()> +{ + // The number of storage items sits right after the 7 note-detail words in the note-data blob. + const NOTE_DATA_NUM_STORAGE_ITEMS_IDX: usize = 7 * WORD_SIZE; + + let assets = vec![NonFungibleAsset::mock(&0u32.to_le_bytes())]; + let input_note = create_public_p2any_note(ACCOUNT_ID_SENDER.try_into()?, assets); + let mut mock_tx = TestTransactionBuilder::with_existing_mock_account() + .input_note(input_note) + .build()?; + + let input_notes_commitment = mock_tx.input_notes().commitment(); + let (_, advice_inputs) = TransactionKernel::prepare_inputs(mock_tx.tx_inputs()); + let note_data = advice_inputs + .as_advice_inputs() + .map + .get(&input_notes_commitment) + .context("input-note advice should be present")? + .as_ref() + .to_vec(); + + let code = " + use miden::tx_kernel_core::prologue + + begin + exec.prologue::prepare_transaction + end + "; + + // The note has empty storage (count == 0): the prologue accepts it because the empty preimage + // hashes to the note's storage commitment. + assert_eq!(note_data[NOTE_DATA_NUM_STORAGE_ITEMS_IDX], ZERO); + mock_tx + .execute_code(code) + .await + .context("valid empty-storage note should be accepted")?; + + // Forge a non-zero storage-item count that the note's storage commitment does not attest to. + // The count stays within the limit, so it bypasses the bounds check and must be caught by + // the commitment verification instead. + let mut note_data = note_data; + note_data[NOTE_DATA_NUM_STORAGE_ITEMS_IDX] = Felt::from(1u32); + mock_tx.set_tx_args(TransactionArgs::new( + BTreeMap::from([(input_notes_commitment, note_data)]).into(), + )); + + let result = mock_tx.execute_code(code).await; + assert_execution_error!(result, ERR_PROLOGUE_NOTE_STORAGE_ITEMS_COUNT_MISMATCH); + + Ok(()) +} + fn global_input_memory_assertions(exec_output: &ExecutionOutput, inputs: &MockTransaction) { assert_eq!( exec_output.get_kernel_mem_word(BLOCK_COMMITMENT_PTR), From 9c218cd68e5fe940d7b5782d8da90a967b9f2bf7 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Thu, 20 Aug 2026 12:24:45 +0000 Subject: [PATCH 2/7] refactor(protocol): name the note storage preimage local offset Addresses review: promote the magic 0 in locaddr.0 to the PROCESS_NOTE_STORAGE_PREIMAGE_LOC constant. Co-Authored-By: Claude Fable 5 --- .../asm/kernels/transaction-core/src/prologue.masm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm index 2611ed152d..07c5b77aea 100644 --- a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm +++ b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm @@ -21,6 +21,10 @@ use {BLOCK_DATA_SECTION_OFFSET, INPUT_VAULT_ROOT_PTR, KERNEL_PROCEDURES_PTR, PAR # Max U32 value, used for initializing the expiration block number const MAX_BLOCK_NUM = 0xFFFFFFFF +# Local in process_note_num_storage_items: buffer to which the note storage preimage is written +# (MAX_NOTE_STORAGE_ITEMS elements) +const PROCESS_NOTE_STORAGE_PREIMAGE_LOC = 0 + # ERRORS # ================================================================================================= @@ -683,7 +687,7 @@ proc process_note_num_storage_items dup.1 exec.memory::get_input_note_storage_commitment # => [NOTE_STORAGE_COMMITMENT, num_storage_items, note_ptr] - locaddr.0 movdn.5 + locaddr.PROCESS_NOTE_STORAGE_PREIMAGE_LOC movdn.5 # => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr, note_ptr] # load the storage preimage from the advice map, padded to the next multiple of 8 so it can be From d3d61b1eb03f3010c424e49d0d6191d4d0d2778f Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Thu, 20 Aug 2026 12:29:53 +0000 Subject: [PATCH 3/7] docs(protocol): reword "advised" item count to count from the advice map Addresses review: "advised" is not used as an adjective in this codebase; refer to the item count from the advice map instead. Co-Authored-By: Claude Fable 5 --- .../asm/kernels/transaction-core/src/prologue.masm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm index 07c5b77aea..7d6ce4a2df 100644 --- a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm +++ b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm @@ -666,7 +666,7 @@ end #! #! Panics if: #! - the number of storage items exceeds the maximum limit. -#! - the advised number of storage items does not match the note's storage commitment. +#! - the number of storage items from the advice map does not match the note's declared count. #! - the note's storage does not match its storage commitment. @locals(1024) proc process_note_num_storage_items @@ -696,7 +696,7 @@ proc process_note_num_storage_items # OS => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr, note_ptr] # AS => [advice_num_storage_items, [STORAGE_ITEMS]] - # assert the advised item count matches the note's declared count + # assert the item count from the advice map matches the note's declared count adv_push dup.5 assert_eq.err=ERR_PROLOGUE_NOTE_STORAGE_ITEMS_COUNT_MISMATCH # => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr, note_ptr] From 117b35cb7264f1a939082e066d2e845ba4391d66 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Thu, 20 Aug 2026 12:36:39 +0000 Subject: [PATCH 4/7] docs(protocol): add stack state comment before pipe_elements_preimage_to_memory Addresses review: document the operand stack state right before the call to pipe_elements_preimage_to_memory. Co-Authored-By: Claude Fable 5 --- .../asm/kernels/transaction-core/src/prologue.masm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm index 7d6ce4a2df..20e2cf4454 100644 --- a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm +++ b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm @@ -702,6 +702,8 @@ proc process_note_num_storage_items # write the preimage to scratch memory and compute its commitment movup.5 movup.5 + # => [num_storage_items, dest_ptr, NOTE_STORAGE_COMMITMENT, note_ptr] + exec.mem_utils::pipe_elements_preimage_to_memory # => [COMPUTED_COMMITMENT, NOTE_STORAGE_COMMITMENT, note_ptr] From e7edb4fa6a3ca79d244b85aaceb84011951d85ad Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Thu, 20 Aug 2026 12:44:02 +0000 Subject: [PATCH 5/7] docs(protocol): refer to procedure locals instead of scratch memory Addresses review: "scratch buffer"/"scratch memory" is not used in this codebase; refer to memory locals / the preimage buffer instead. Co-Authored-By: Claude Fable 5 --- .../asm/kernels/transaction-core/src/prologue.masm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm index 20e2cf4454..dc5cb5e128 100644 --- a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm +++ b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm @@ -662,7 +662,7 @@ end #! - STORAGE_ITEMS are the note's storage items. #! #! Locals: -#! - 0..1024: scratch buffer for the storage preimage (MAX_NOTE_STORAGE_ITEMS elements) +#! - 0..1024: buffer to which the storage preimage is written (MAX_NOTE_STORAGE_ITEMS elements) #! #! Panics if: #! - the number of storage items exceeds the maximum limit. @@ -683,7 +683,7 @@ proc process_note_num_storage_items dup dup.2 exec.memory::set_input_note_num_storage_items # => [num_storage_items, note_ptr] - # load the note's authenticated storage commitment and a scratch pointer for the preimage + # load the note's authenticated storage commitment and a pointer to the preimage memory locals dup.1 exec.memory::get_input_note_storage_commitment # => [NOTE_STORAGE_COMMITMENT, num_storage_items, note_ptr] @@ -700,7 +700,7 @@ proc process_note_num_storage_items adv_push dup.5 assert_eq.err=ERR_PROLOGUE_NOTE_STORAGE_ITEMS_COUNT_MISMATCH # => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr, note_ptr] - # write the preimage to scratch memory and compute its commitment + # write the preimage to the memory locals and compute its commitment movup.5 movup.5 # => [num_storage_items, dest_ptr, NOTE_STORAGE_COMMITMENT, note_ptr] From e44e0a0593be6da4d1cdab8e0aa82c2d3049438d Mon Sep 17 00:00:00 2001 From: Marti Date: Thu, 20 Aug 2026 16:17:29 +0200 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: Marti --- .../asm/kernels/transaction-core/src/prologue.masm | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm index dc5cb5e128..be6ff56a2b 100644 --- a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm +++ b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm @@ -21,8 +21,7 @@ use {BLOCK_DATA_SECTION_OFFSET, INPUT_VAULT_ROOT_PTR, KERNEL_PROCEDURES_PTR, PAR # Max U32 value, used for initializing the expiration block number const MAX_BLOCK_NUM = 0xFFFFFFFF -# Local in process_note_num_storage_items: buffer to which the note storage preimage is written -# (MAX_NOTE_STORAGE_ITEMS elements) +# Procedure locals in process_note_num_storage_items const PROCESS_NOTE_STORAGE_PREIMAGE_LOC = 0 # ERRORS @@ -679,7 +678,7 @@ proc process_note_num_storage_items assert.err=ERR_PROLOGUE_NUMBER_OF_NOTE_STORAGE_ITEMS_EXCEEDED_LIMIT # => [num_storage_items, note_ptr] - # store the number of storage items into the memory, keeping the count on the stack + # store the number of storage items into the memory dup dup.2 exec.memory::set_input_note_num_storage_items # => [num_storage_items, note_ptr] @@ -690,8 +689,7 @@ proc process_note_num_storage_items locaddr.PROCESS_NOTE_STORAGE_PREIMAGE_LOC movdn.5 # => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr, note_ptr] - # load the storage preimage from the advice map, padded to the next multiple of 8 so it can be - # written with `pipe_elements_preimage_to_memory` + # load the storage preimage from the advice map, padded to the next multiple of 8 adv.push_mapvaln.8 # OS => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr, note_ptr] # AS => [advice_num_storage_items, [STORAGE_ITEMS]] @@ -707,7 +705,6 @@ proc process_note_num_storage_items exec.mem_utils::pipe_elements_preimage_to_memory # => [COMPUTED_COMMITMENT, NOTE_STORAGE_COMMITMENT, note_ptr] - # assert the note's storage matches its authenticated commitment assert_eqw.err=ERR_PROLOGUE_NOTE_STORAGE_DOES_NOT_MATCH_COMMITMENT # => [note_ptr] end From 08356c03084b56efcd47acb51f8ffb45a9d23f9a Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Fri, 21 Aug 2026 06:56:06 +0000 Subject: [PATCH 7/7] docs(protocol): note that push_mapvaln also pushes the element count --- .../asm/kernels/transaction-core/src/prologue.masm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm index eda1b00a38..45da6d3ded 100644 --- a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm +++ b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm @@ -689,7 +689,8 @@ proc process_note_num_storage_items locaddr.PROCESS_NOTE_STORAGE_PREIMAGE_LOC movdn.5 # => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr, note_ptr] - # load the storage preimage from the advice map, padded to the next multiple of 8 + # load the storage preimage from the advice map onto the advice stack, prefixed with its + # element count and padded to the next multiple of 8 adv.push_mapvaln.8 # OS => [NOTE_STORAGE_COMMITMENT, num_storage_items, dest_ptr, note_ptr] # AS => [advice_num_storage_items, [STORAGE_ITEMS]]