From e4da31afa00c38d1c2684c1893157ee935865042 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 26 Aug 2026 12:02:18 +0200 Subject: [PATCH 1/3] chore: validate note version in prologue --- .../kernels/transaction-core/src/note.masm | 2 +- .../transaction-core/src/prologue.masm | 20 +++-- .../asm/protocol_utils/src/note.masm | 89 ++++++++++++++++++- .../src/kernel_tests/tx/test_note.rs | 83 +++++++++++++++++ 4 files changed, 185 insertions(+), 9 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction-core/src/note.masm b/crates/miden-protocol/asm/kernels/transaction-core/src/note.masm index ab1d2f32fb..468fcfbe50 100644 --- a/crates/miden-protocol/asm/kernels/transaction-core/src/note.masm +++ b/crates/miden-protocol/asm/kernels/transaction-core/src/note.masm @@ -4,7 +4,7 @@ use {ASSET_SIZE} from miden::tx_kernel_core::asset use {NOTE_MEM_SIZE, WORD_NUM_ELEMENTS} from miden::tx_kernel_core::constants use miden::tx_kernel_core::memory -pub use {ATTACHMENT_SCHEME_NONE, MAX_ATTACHMENT_SCHEME, MAX_ATTACHMENT_TOTAL_WORDS, MAX_ATTACHMENT_WORDS, NOTE_METADATA_VERSION_1, NOTE_TYPE_PRIVATE, NOTE_TYPE_PUBLIC} +pub use {ATTACHMENT_SCHEME_NONE, MAX_ATTACHMENT_SCHEME, MAX_ATTACHMENT_TOTAL_WORDS, MAX_ATTACHMENT_WORDS, NOTE_METADATA_VERSION_1, NOTE_TYPE_PRIVATE, NOTE_TYPE_PUBLIC, validate_metadata, metadata_into_version} from miden::protocol_utils::note # ERRORS 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 f34c2f0824..ed3985bef6 100644 --- a/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm +++ b/crates/miden-protocol/asm/kernels/transaction-core/src/prologue.masm @@ -15,6 +15,7 @@ use {EMPTY_SMT_ROOT, MAX_ASSETS_PER_NOTE, MAX_INPUT_NOTES_PER_TX, MAX_NOTE_STORA use miden::tx_kernel_core::memory use {BLOCK_DATA_SECTION_OFFSET, INPUT_VAULT_ROOT_PTR, KERNEL_PROCEDURES_PTR, PARTIAL_BLOCKCHAIN_PTR} from miden::tx_kernel_core::memory +use miden::tx_kernel_core::note # CONSTS # ================================================================================================= @@ -631,7 +632,8 @@ proc process_note_args # => [note_ptr] end -#! Computes the note metadata commitment from metadata word and attachments commitment. +#! Validates the note metadata and computes its commitment from the metadata word and the +#! attachments commitment. #! #! Inputs: [note_ptr] #! Outputs: [NOTE_METADATA_COMMITMENT] @@ -640,7 +642,11 @@ end #! - note_ptr is the memory location for the input note. #! - NOTE_METADATA_COMMITMENT is the commitment to the note metadata computed as #! `hash(NOTE_METADATA || NOTE_ATTACHMENTS_COMMITMENT)`. -proc compute_note_metadata_commitment +#! +#! Panics if: +#! - the metadata encodes an unknown version. +#! - the metadata has its reserved bit set. +proc process_note_metadata # load the attachments commitment onto the stack dup exec.memory::get_input_note_attachments_commitment # => [NOTE_ATTACHMENTS_COMMITMENT, note_ptr] @@ -649,6 +655,10 @@ proc compute_note_metadata_commitment movup.4 exec.memory::get_input_note_metadata # => [NOTE_METADATA, NOTE_ATTACHMENTS_COMMITMENT] + # the version defines how the metadata is decoded, so assert it before anything reads it + dupw exec.note::validate_metadata + # => [NOTE_METADATA, NOTE_ATTACHMENTS_COMMITMENT] + exec.poseidon2::merge # => [NOTE_METADATA_COMMITMENT] end @@ -943,11 +953,11 @@ proc process_input_note dup exec.add_input_note_assets_to_vault # => [note_ptr, NULLIFIER, CAPACITY] - # note metadata commitment + # note metadata # --------------------------------------------------------------------------------------------- - # compute NOTE_METADATA_COMMITMENT - dup exec.compute_note_metadata_commitment + # validate the metadata and compute NOTE_METADATA_COMMITMENT + dup exec.process_note_metadata # => [NOTE_METADATA_COMMITMENT, note_ptr, NULLIFIER, CAPACITY] # note details commitment diff --git a/crates/miden-protocol/asm/protocol_utils/src/note.masm b/crates/miden-protocol/asm/protocol_utils/src/note.masm index aed3fd3f4a..0863faee56 100644 --- a/crates/miden-protocol/asm/protocol_utils/src/note.masm +++ b/crates/miden-protocol/asm/protocol_utils/src/note.masm @@ -1,15 +1,28 @@ +# ERRORS +# ================================================================================================= + +const ERR_NOTE_METADATA_UNSUPPORTED_VERSION = "note metadata has an unsupported version" + +const ERR_NOTE_METADATA_NON_ZERO_RESERVED_BIT = "note metadata has a non-zero reserved bit" + # CONSTANTS # ================================================================================================= # The maximum number of storage values associated with a single note. pub const MAX_NOTE_STORAGE_ITEMS = 1024 -# Note type constants. These encode the note type in the lower byte of the metadata. -# See NoteType in the Rust protocol crate for details. - #! Version 1 of the note metadata encoding. pub const NOTE_METADATA_VERSION_1 = 1 +#! The mask for the version bits in the first felt of the note metadata. +const NOTE_METADATA_VERSION_MASK = 0x3f # 0b0011_1111 + +#! The mask for the reserved bit in the first felt of the note metadata. +const NOTE_METADATA_RESERVED_BIT_MASK = 0x80 # 0b1000_0000 + +# Note type constants. These encode the note type in the lower byte of the metadata. +# See NoteType in the Rust protocol crate for details. + #! The note type of private notes. pub const NOTE_TYPE_PRIVATE = 0 @@ -27,3 +40,73 @@ pub const MAX_ATTACHMENT_TOTAL_WORDS = 512 #! The reserved value to signal a `None` note attachment scheme. pub const ATTACHMENT_SCHEME_NONE = 1 + +# PROCEDURES +# ================================================================================================= + +#! Extracts the version from the provided metadata. +#! +#! Inputs: [METADATA] +#! Outputs: [version] +#! +#! Where: +#! - METADATA is the metadata of a note. +#! - version is the version of the note metadata encoding. +pub proc metadata_into_version + movdn.3 drop drop drop + # => [sender_id_suffix_type_version] + + u32split swap drop + # => [lo32] + + u32and.NOTE_METADATA_VERSION_MASK + # => [version] +end + +#! Extracts the reserved bit from the provided metadata. +#! +#! Inputs: [METADATA] +#! Outputs: [reserved_bit] +#! +#! Where: +#! - METADATA is the metadata of a note. +#! - reserved_bit is 1 if the reserved bit is set, 0 otherwise. +proc metadata_into_reserved_bit + movdn.3 drop drop drop + # => [sender_id_suffix_type_version] + + u32split swap drop + # => [lo32] + + u32and.NOTE_METADATA_RESERVED_BIT_MASK neq.0 + # => [reserved_bit] +end + +#! Validates that note metadata is well formed and consumes it. +#! +#! WARNING: This procedure should not be exposed to or called from user code (e.g. miden::protocol +#! or miden::standards) as this would make the calling code only accept note metadata version 1. +#! +#! Inputs: [METADATA] +#! Outputs: [] +#! +#! Where: +#! - METADATA is the metadata of a note. +#! +#! Panics if: +#! - the metadata encodes an unknown version. +#! - the metadata has its reserved bit set. +pub proc validate_metadata + # the version defines how the rest of the metadata is decoded, so assert it first + dupw exec.metadata_into_version + # => [version, METADATA] + + eq.NOTE_METADATA_VERSION_1 assert.err=ERR_NOTE_METADATA_UNSUPPORTED_VERSION + # => [METADATA] + + exec.metadata_into_reserved_bit + # => [reserved_bit] + + assertz.err=ERR_NOTE_METADATA_NON_ZERO_RESERVED_BIT + # => [] +end diff --git a/crates/miden-testing/src/kernel_tests/tx/test_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_note.rs index 11cbaf4886..bb6e4042c9 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_note.rs @@ -10,6 +10,10 @@ use miden_protocol::asset::FungibleAsset; use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey; use miden_protocol::crypto::rand::{FeltRng, RandomCoin}; use miden_protocol::errors::MasmError; +use miden_protocol::errors::protocol::{ + ERR_NOTE_METADATA_NON_ZERO_RESERVED_BIT, + ERR_NOTE_METADATA_UNSUPPORTED_VERSION, +}; use miden_protocol::note::{ Note, NoteAssets, @@ -45,6 +49,7 @@ use crate::{ MockChain, MockTransaction, TestTransactionBuilder, + assert_execution_error, assert_transaction_executor_error, }; @@ -693,3 +698,81 @@ async fn test_find_attachment_idx( Ok(()) } + +#[rstest::rstest] +#[case::private(NoteType::Private)] +#[case::public(NoteType::Public)] +#[tokio::test] +async fn test_metadata_into_version(#[case] note_type: NoteType) -> anyhow::Result<()> { + let sender = AccountId::try_from(ACCOUNT_ID_SENDER)?; + let partial_metadata = PartialNoteMetadata::new(sender, note_type); + let metadata = NoteMetadata::new(partial_metadata, &NoteAttachments::default()); + let metadata_word = metadata.to_metadata_word(); + + let code = format!( + " + use miden::tx_kernel_core::note + + begin + push.{metadata_word} + exec.note::metadata_into_version + # => [version, pad(16)] + + # truncate the stack + swap drop + end + ", + ); + + let exec_output = CodeExecutor::with_default_host().run(&code).await?; + + // The metadata encoder only ever writes version 1. + assert_eq!(exec_output.get_stack_element(0), Felt::from(1u8)); + + Ok(()) +} + +/// Tests that `validate_metadata` accepts version 1 metadata and rejects metadata with an unknown +/// version or with its reserved bit set. +#[rstest::rstest] +#[case::valid_private(0b0000_0001, None)] +#[case::valid_public(0b0100_0001, None)] +#[case::version_zero(0b0000_0000, Some(ERR_NOTE_METADATA_UNSUPPORTED_VERSION))] +#[case::unknown_version(0b0000_0010, Some(ERR_NOTE_METADATA_UNSUPPORTED_VERSION))] +#[case::reserved_bit_set(0b1000_0001, Some(ERR_NOTE_METADATA_NON_ZERO_RESERVED_BIT))] +#[tokio::test] +async fn test_validate_note_metadata( + #[case] metadata_byte: u8, + #[case] expected_err: Option, +) -> anyhow::Result<()> { + let sender = AccountId::try_from(ACCOUNT_ID_SENDER)?; + let partial_metadata = PartialNoteMetadata::new(sender, NoteType::Public); + let metadata = NoteMetadata::new(partial_metadata, &NoteAttachments::default()); + let mut metadata_word = metadata.to_metadata_word(); + + // The lower byte of the sender ID suffix is zero by construction, so it can be replaced by the + // byte holding the version, note type and reserved bit. + metadata_word[0] = sender.suffix() + Felt::from(metadata_byte); + + let code = format!( + " + use miden::tx_kernel_core::note + + begin + push.{metadata_word} + exec.note::validate_metadata + end + ", + ); + + let exec_result = CodeExecutor::with_default_host().run(&code).await; + + match expected_err { + Some(err) => assert_execution_error!(exec_result, err), + None => { + exec_result.context("version 1 metadata should be accepted")?; + }, + } + + Ok(()) +} From 84c874d1f6197f1f60c5cd715f23d1cca60698b4 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 26 Aug 2026 12:04:32 +0200 Subject: [PATCH 2/3] chore: add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cc4392567..6d76f404fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - [BREAKING] `NoteScript::from_parts` and `TransactionScript::from_parts` now return a `Result` instead of panicking when the specified entrypoint is not in the provided MAST forest ([#3548](https://github.com/0xMiden/protocol/pull/3548)). - [BREAKING] Sorted the procedures of `AccountCode` after the authentication procedure at index 0, making the account code commitment independent of the order in which components are provided ([#2961](https://github.com/0xMiden/protocol/pull/3565)). - The transaction kernel now validates that a new account's procedures are sorted and unique ([#3567](https://github.com/0xMiden/protocol/pull/3567)). +- The transaction kernel now validates the version and reserved bit of every input note's metadata ([#3742](https://github.com/0xMiden/protocol/pull/3742)). - [BREAKING] Renamed the fungible asset amount extraction procedures so the unsuffixed name is the validating one ([#3576](https://github.com/0xMiden/protocol/pull/3576)): - `miden::protocol::asset::fungible_value_into_amount` -> `fungible_value_into_amount_unchecked`. - `miden::standards::assets::fungible_asset::value_into_amount` to `value_into_amount_unchecked`. From 64344f97b00f84176ac548142317193a7dea4e86 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 26 Aug 2026 18:05:19 +0200 Subject: [PATCH 3/3] Update CHANGELOG.md Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d76f404fb..dfc1a028be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ - [BREAKING] `NoteScript::from_parts` and `TransactionScript::from_parts` now return a `Result` instead of panicking when the specified entrypoint is not in the provided MAST forest ([#3548](https://github.com/0xMiden/protocol/pull/3548)). - [BREAKING] Sorted the procedures of `AccountCode` after the authentication procedure at index 0, making the account code commitment independent of the order in which components are provided ([#2961](https://github.com/0xMiden/protocol/pull/3565)). - The transaction kernel now validates that a new account's procedures are sorted and unique ([#3567](https://github.com/0xMiden/protocol/pull/3567)). -- The transaction kernel now validates the version and reserved bit of every input note's metadata ([#3742](https://github.com/0xMiden/protocol/pull/3742)). +- The transaction kernel now validates the version and reserved bit of every input note's metadata ([#3743](https://github.com/0xMiden/protocol/pull/3743)). - [BREAKING] Renamed the fungible asset amount extraction procedures so the unsuffixed name is the validating one ([#3576](https://github.com/0xMiden/protocol/pull/3576)): - `miden::protocol::asset::fungible_value_into_amount` -> `fungible_value_into_amount_unchecked`. - `miden::standards::assets::fungible_asset::value_into_amount` to `value_into_amount_unchecked`.