Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1720a4b
feat: Define `NoteTag` to wrap arbitrary `u32`
PhilippGackstatter Dec 26, 2025
81cb9a6
feat: Rework note tag docs and constructors
PhilippGackstatter Jan 2, 2026
bc4db70
chore: Remove use case constructors and refactor swap tag
PhilippGackstatter Jan 2, 2026
afaf21d
feat: Remove note tag validation
PhilippGackstatter Jan 2, 2026
808fbe7
feat: Rename account ID conversion to account target
PhilippGackstatter Jan 2, 2026
ddaa1b2
feat: Update note tag docs
PhilippGackstatter Jan 2, 2026
e893da5
chore: add changelog
PhilippGackstatter Jan 2, 2026
2866c3e
chore: Make `NoteMetadata::new` infallible
PhilippGackstatter Jan 2, 2026
bd55de3
chore: address review comments on docs
PhilippGackstatter Jan 7, 2026
c1bec10
Merge remote-tracking branch 'origin/next' into pgackst-note-tag-refa…
PhilippGackstatter Jan 7, 2026
466a4b7
chore: remove outdated note tag rules in metadata docs
PhilippGackstatter Jan 7, 2026
24a9f3e
Merge branch 'next' into pgackst-note-tag-refactor
bobbinth Jan 8, 2026
e9e8c22
chore: remove `create_mock_notes` (#2236)
PhilippGackstatter Jan 8, 2026
d35aa8e
Merge remote-tracking branch 'origin/next' into pgackst-note-tag-refa…
PhilippGackstatter Jan 8, 2026
2297716
Merge remote-tracking branch 'origin/next' into pgackst-note-tag-refa…
PhilippGackstatter Jan 12, 2026
48475c3
Merge remote-tracking branch 'origin/next' into pgackst-note-tag-refa…
PhilippGackstatter Jan 13, 2026
f925c8b
Merge remote-tracking branch 'origin/next' into pgackst-note-tag-refa…
PhilippGackstatter Jan 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
- [BREAKING] Refactored `AccountStorageDelta` to use a new `StorageSlotDelta` type ([#2182](https://github.com/0xMiden/miden-base/pull/2182)).
- [BREAKING] Removed OLD_MAP_ROOT from being returned when calling [`native_account::set_map_item`](crates/miden-lib/asm/miden/native_account.masm) ([#2194](https://github.com/0xMiden/miden-base/pull/2194)).
- [BREAKING] Refactored account component templates into `AccountStorageSchema` ([#2193](https://github.com/0xMiden/miden-base/pull/2193)).
- [BREAKING] Refactor note tags to be arbitrary `u32` values and drop previous validation ([#2219](https://github.com/0xMiden/miden-base/pull/2219)).

## 0.12.4 (2025-11-26)

Expand Down
41 changes: 3 additions & 38 deletions crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ const PUBLIC_NOTE=1 # 0b01
const PRIVATE_NOTE=2 # 0b10
const ENCRYPTED_NOTE=3 # 0b11

# The note type must be PUBLIC, unless the high bits are `0b11`. (See the table below.)
const LOCAL_ANY_PREFIX=3 # 0b11

# ERRORS
# =================================================================================================

Expand All @@ -31,29 +28,7 @@ const ERR_NOTE_FUNGIBLE_MAX_AMOUNT_EXCEEDED="adding a fungible asset to a note c

const ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS="non-fungible asset that already exists in the note cannot be added again"

# The 2 highest bits in the u32 tag have the following meaning:
#
# | Prefix | Name | [`NoteExecutionMode`] | Target | Allowed [`NoteType`] |
# | :----: | :--------------------: | :-------------------: | :----------------------: | :------------------: |
# | `0b00` | `NetworkAccount` | Network | Network Account | [`NoteType::Public`] |
# | `0b01` | `NetworkUseCase` | Network | Use case | [`NoteType::Public`] |
# | `0b10` | `LocalPublicAny` | Local | Any | [`NoteType::Public`] |
# | `0b11` | `LocalAny` | Local | Any | Any |
#
# Execution: Is a hint for the network, to check if the note can be consumed by a network controlled
# account
# Target: Is a hint for the type of target. Use case means the note may be consumed by anyone,
# specific means there is a specific target for the note (the target may be a public key, a user
# that knows some secret, or a specific account ID)
#
# Only the note type from the above list is enforced. The other values are only hints intended as a
# best effort optimization strategy. A badly formatted note may 1. not be consumed because honest
# users won't see the note 2. generate slightly more load as extra validation is performed for the
# invalid tags. None of these scenarios have any significant impact.

const ERR_NOTE_INVALID_NOTE_TYPE_FOR_NOTE_TAG_PREFIX="invalid note type for the given note tag prefix"

const ERR_NOTE_TAG_MUST_BE_U32="the note's tag must fit into a u32 so the 32 most significant bits must be zero"
const ERR_NOTE_TAG_MUST_BE_U32="the note's tag must fit into a u32 so the 32 most significant bits of the felt must be zero"

# EVENTS
# =================================================================================================
Expand Down Expand Up @@ -86,9 +61,8 @@ const NOTE_AFTER_ADD_ASSET_EVENT=event("miden::note::after_add_asset")
#! - note_idx is the index of the created note.
#!
#! Panics if:
#! - the note_type is not valid.
#! - the note_tag is not an u32.
#! - the note_tag starts with anything but 0b11 and note_type is not public.
#! - the note_type is unknown.
#! - the note tag is not a u32.
#! - the number of output notes exceeds the maximum limit of 1024.
pub proc create
emit.NOTE_BEFORE_CREATED_EVENT
Expand Down Expand Up @@ -278,16 +252,7 @@ pub proc build_metadata
dup.2 eq.PRIVATE_NOTE dup.3 eq.PUBLIC_NOTE or assert.err=ERR_NOTE_INVALID_TYPE
# => [tag, aux, note_type, execution_hint]

# copy data to validate the tag
dup.2 push.PUBLIC_NOTE dup.1 dup.3
# => [tag, note_type, public_note, note_type, tag, aux, note_type, execution_hint]

u32assert.err=ERR_NOTE_TAG_MUST_BE_U32
# => [tag, note_type, public_note, note_type, tag, aux, note_type, execution_hint]

# enforce the note type depending on the tag' bits
u32shr.30 eq.LOCAL_ANY_PREFIX cdrop
assert_eq.err=ERR_NOTE_INVALID_NOTE_TYPE_FOR_NOTE_TAG_PREFIX
# => [tag, aux, note_type, execution_hint]

# Split execution hint into its tag and payload parts as they are encoded in separate elements
Expand Down
5 changes: 5 additions & 0 deletions crates/miden-protocol/asm/protocol/output_note.masm
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ use miden::protocol::note
#! - RECIPIENT is the recipient of the note.
#! - note_idx is the index of the created note.
#!
#! Panics if:
#! - the note_type is unknown.
#! - the note tag is not a u32.
#! - the number of output notes exceeds the maximum limit of 1024.
#!
#! Invocation: exec
pub proc create
# pad the stack before the syscall to prevent accidental modification of the deeper stack
Expand Down
7 changes: 4 additions & 3 deletions crates/miden-protocol/src/address/address_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@ impl AddressId {
/// Returns the default tag length of the ID.
///
/// This is guaranteed to be in range `0..=30` (e.g. the maximum of
/// [`NoteTag::MAX_LOCAL_TAG_LENGTH`] and [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`]).
/// [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`] and
/// [`NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH`]).
pub fn default_note_tag_len(&self) -> u8 {
match self {
AddressId::AccountId(id) => {
if id.storage_mode() == AccountStorageMode::Network {
NoteTag::DEFAULT_NETWORK_TAG_LENGTH
NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH
} else {
NoteTag::DEFAULT_LOCAL_TAG_LENGTH
NoteTag::DEFAULT_LOCAL_ACCOUNT_TARGET_TAG_LENGTH
}
},
}
Expand Down
17 changes: 9 additions & 8 deletions crates/miden-protocol/src/address/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ impl Address {
/// # Errors
///
/// Returns an error if:
/// - The tag length routing parameter is not [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`] for
/// network accounts.
/// - The tag length routing parameter is not
/// [`NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH`] for network accounts.
pub fn with_routing_parameters(
mut self,
routing_params: RoutingParameters,
Expand All @@ -95,7 +95,7 @@ impl Address {
match self.id {
AddressId::AccountId(account_id) => {
if account_id.storage_mode() == AccountStorageMode::Network
&& tag_len != NoteTag::DEFAULT_NETWORK_TAG_LENGTH
&& tag_len != NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH
{
return Err(AddressError::CustomTagLengthNotAllowedForNetworkAccounts(
tag_len,
Expand Down Expand Up @@ -126,7 +126,8 @@ impl Address {
/// Returns the preferred tag length.
///
/// This is guaranteed to be in range `0..=30` (e.g. the maximum of
/// [`NoteTag::MAX_LOCAL_TAG_LENGTH`] and [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`]).
/// [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`] and
/// [`NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH`]).
pub fn note_tag_len(&self) -> u8 {
self.routing_params
.as_ref()
Expand All @@ -143,8 +144,8 @@ impl Address {
match id.storage_mode() {
AccountStorageMode::Network => NoteTag::from_network_account_id(id),
AccountStorageMode::Private | AccountStorageMode::Public => {
NoteTag::from_local_account_id(id, note_tag_len)
.expect("address should validate that tag len does not exceed MAX_LOCAL_TAG_LENGTH bits")
NoteTag::with_custom_account_target(id, note_tag_len)
.expect("address should validate that tag len does not exceed MAX_ACCOUNT_TARGET_TAG_LENGTH bits")
}
}
},
Expand Down Expand Up @@ -302,7 +303,7 @@ mod tests {
// Encode/Decode with routing parameters should be valid.
address = address.with_routing_parameters(
RoutingParameters::new(AddressInterface::BasicWallet)
.with_note_tag_len(NoteTag::DEFAULT_NETWORK_TAG_LENGTH)?,
.with_note_tag_len(NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH)?,
)?;

let bech32_string = address.encode(network_id.clone());
Expand Down Expand Up @@ -431,7 +432,7 @@ mod tests {
let account_id = AccountIdBuilder::new().account_type(account_type).build_with_rng(rng);
let address = Address::new(account_id).with_routing_parameters(
RoutingParameters::new(AddressInterface::BasicWallet)
.with_note_tag_len(NoteTag::DEFAULT_NETWORK_TAG_LENGTH)?,
.with_note_tag_len(NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH)?,
)?;

let serialized = address.to_bytes();
Expand Down
21 changes: 11 additions & 10 deletions crates/miden-protocol/src/address/routing_parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,16 @@ impl RoutingParameters {
/// The tag length determines how many bits of the address ID are encoded into [`NoteTag`]s of
/// notes targeted to this address. This lets the receiver choose their level of privacy. A
/// higher tag length makes the address ID more uniquely identifiable and reduces privacy,
/// while a shorter length increases privacy at the cost of matching more notes
/// published onchain.
/// while a shorter length increases privacy at the cost of matching more notes published
/// onchain.
///
/// # Errors
///
/// Returns an error if:
/// - The tag length exceeds the maximum of [`NoteTag::MAX_LOCAL_TAG_LENGTH`] and
/// [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`].
/// - The tag length exceeds the maximum of [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`] and
/// [`NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH`].
pub fn with_note_tag_len(mut self, note_tag_len: u8) -> Result<Self, AddressError> {
if note_tag_len > NoteTag::MAX_LOCAL_TAG_LENGTH {
if note_tag_len > NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH {
return Err(AddressError::TagLengthTooLarge(note_tag_len));
}

Expand All @@ -110,7 +110,8 @@ impl RoutingParameters {
/// Returns the note tag length preference.
///
/// This is guaranteed to be in range `0..=30` (e.g. the maximum of
/// [`NoteTag::MAX_LOCAL_TAG_LENGTH`] and [`NoteTag::DEFAULT_NETWORK_TAG_LENGTH`]).
/// [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`] and
/// [`NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH`]).
pub fn note_tag_len(&self) -> Option<u8> {
self.note_tag_len
}
Expand Down Expand Up @@ -467,11 +468,11 @@ mod tests {

// Test case 4: Explicit tag length set to max
let params_tag_max = RoutingParameters::new(AddressInterface::BasicWallet)
.with_note_tag_len(NoteTag::MAX_LOCAL_TAG_LENGTH)?;
.with_note_tag_len(NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH)?;
let encoded = params_tag_max.encode_to_string();
let decoded = RoutingParameters::decode(encoded)?;
assert_eq!(params_tag_max, decoded);
assert_eq!(decoded.note_tag_len(), Some(NoteTag::MAX_LOCAL_TAG_LENGTH));
assert_eq!(decoded.note_tag_len(), Some(NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH));

Ok(())
}
Expand Down Expand Up @@ -504,11 +505,11 @@ mod tests {

// Test case 4: Explicit tag length set to max
let params_tag_max = RoutingParameters::new(AddressInterface::BasicWallet)
.with_note_tag_len(NoteTag::MAX_LOCAL_TAG_LENGTH)?;
.with_note_tag_len(NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH)?;
let serialized = params_tag_max.to_bytes();
let deserialized = RoutingParameters::read_from_bytes(&serialized)?;
assert_eq!(params_tag_max, deserialized);
assert_eq!(deserialized.note_tag_len(), Some(NoteTag::MAX_LOCAL_TAG_LENGTH));
assert_eq!(deserialized.note_tag_len(), Some(NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH));

Ok(())
}
Expand Down
8 changes: 3 additions & 5 deletions crates/miden-protocol/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,11 @@ pub enum AccountTreeError {
#[derive(Debug, Error)]
pub enum AddressError {
#[error("tag length {0} should be {expected} bits for network accounts",
expected = NoteTag::DEFAULT_NETWORK_TAG_LENGTH
expected = NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH
)]
CustomTagLengthNotAllowedForNetworkAccounts(u8),
#[error("tag length {0} is too large, must be less than or equal to {max}",
max = NoteTag::MAX_LOCAL_TAG_LENGTH
max = NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH
)]
TagLengthTooLarge(u8),
#[error("unknown address interface `{0}`")]
Expand Down Expand Up @@ -540,7 +540,7 @@ pub enum PartialAssetVaultError {

#[derive(Debug, Error)]
pub enum NoteError {
#[error("note tag length {0} exceeds the maximum of {max}", max = NoteTag::MAX_LOCAL_TAG_LENGTH)]
#[error("note tag length {0} exceeds the maximum of {max}", max = NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH)]
NoteTagLengthTooLarge(u8),
#[error("duplicate fungible asset from issuer {0} in note")]
DuplicateFungibleAsset(AccountId),
Expand All @@ -552,8 +552,6 @@ pub enum NoteError {
AddFungibleAssetBalanceError(#[source] AssetError),
#[error("note sender is not a valid account ID")]
NoteSenderInvalidAccountId(#[source] AccountIdError),
#[error("note tag use case {0} must be less than 2^{exp}", exp = NoteTag::MAX_USE_CASE_ID_EXPONENT)]
NoteTagUseCaseTooLarge(u16),
#[error(
"note execution hint tag {0} must be in range {from}..={to}",
from = NoteExecutionHint::NONE_TAG,
Expand Down
6 changes: 2 additions & 4 deletions crates/miden-protocol/src/errors/tx_kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,12 @@ pub const ERR_NOTE_ATTEMPT_TO_ACCESS_NOTE_SERIAL_NUMBER_WHILE_NO_NOTE_BEING_PROC
pub const ERR_NOTE_FUNGIBLE_MAX_AMOUNT_EXCEEDED: MasmError = MasmError::from_static_str("adding a fungible asset to a note cannot exceed the max_amount of 9223372036854775807");
/// Error Message: "failed to find note at the given index; index must be within [0, num_of_notes]"
pub const ERR_NOTE_INVALID_INDEX: MasmError = MasmError::from_static_str("failed to find note at the given index; index must be within [0, num_of_notes]");
/// Error Message: "invalid note type for the given note tag prefix"
pub const ERR_NOTE_INVALID_NOTE_TYPE_FOR_NOTE_TAG_PREFIX: MasmError = MasmError::from_static_str("invalid note type for the given note tag prefix");
/// Error Message: "invalid note type"
pub const ERR_NOTE_INVALID_TYPE: MasmError = MasmError::from_static_str("invalid note type");
/// Error Message: "number of assets in a note exceed 255"
pub const ERR_NOTE_NUM_OF_ASSETS_EXCEED_LIMIT: MasmError = MasmError::from_static_str("number of assets in a note exceed 255");
/// Error Message: "the note's tag must fit into a u32 so the 32 most significant bits must be zero"
pub const ERR_NOTE_TAG_MUST_BE_U32: MasmError = MasmError::from_static_str("the note's tag must fit into a u32 so the 32 most significant bits must be zero");
/// Error Message: "the note's tag must fit into a u32 so the 32 most significant bits of the felt must be zero"
pub const ERR_NOTE_TAG_MUST_BE_U32: MasmError = MasmError::from_static_str("the note's tag must fit into a u32 so the 32 most significant bits of the felt must be zero");

/// Error Message: "requested output note index should be less than the total number of created output notes"
pub const ERR_OUTPUT_NOTE_INDEX_OUT_OF_BOUNDS: MasmError = MasmError::from_static_str("requested output note index should be less than the total number of created output notes");
Expand Down
3 changes: 1 addition & 2 deletions crates/miden-protocol/src/note/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,7 @@ mod tests {
NoteTag::from(123),
crate::note::NoteExecutionHint::None,
Felt::new(0),
)
.unwrap();
);

Note::new(NoteAssets::new(vec![asset]).unwrap(), metadata, recipient)
}
Expand Down
23 changes: 7 additions & 16 deletions crates/miden-protocol/src/note/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ use super::{

/// Metadata associated with a note.
///
/// Note type and tag must be internally consistent according to the following rules:
///
/// - For private and encrypted notes, the two most significant bits of the tag must be `0b11`.
/// - For public notes, the two most significant bits of the tag can be set to any value.
///
/// # Word layout & validity
///
/// [`NoteMetadata`] can be encoded into a [`Word`] with the following layout:
Expand Down Expand Up @@ -65,25 +60,21 @@ pub struct NoteMetadata {
}

impl NoteMetadata {
/// Returns a new [NoteMetadata] instantiated with the specified parameters.
///
/// # Errors
/// Returns an error if the note type and note tag are inconsistent.
/// Returns a new [`NoteMetadata`] instantiated with the specified parameters.
pub fn new(
sender: AccountId,
note_type: NoteType,
tag: NoteTag,
execution_hint: NoteExecutionHint,
aux: Felt,
) -> Result<Self, NoteError> {
let tag = tag.validate(note_type)?;
Ok(Self {
) -> Self {
Self {
sender,
note_type,
tag,
aux,
execution_hint,
})
}
}

/// Returns the account which created the note.
Expand Down Expand Up @@ -162,7 +153,7 @@ impl TryFrom<Word> for NoteMetadata {
let (execution_hint, note_tag) =
unmerge_note_tag_and_hint_payload(elements[2], execution_hint_tag)?;

Self::new(sender, note_type, note_tag, execution_hint, elements[3])
Ok(Self::new(sender, note_type, note_tag, execution_hint, elements[3]))
}
}

Expand Down Expand Up @@ -309,7 +300,7 @@ mod tests {
// produces valid felts.
let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap();
let note_type = NoteType::Public;
let tag = NoteTag::from_account_id(sender);
let tag = NoteTag::with_account_target(sender);
let aux = Felt::try_from(0xffff_ffff_0000_0000u64).unwrap();

for execution_hint in [
Expand All @@ -318,7 +309,7 @@ mod tests {
NoteExecutionHint::on_block_slot(10, 11, 12),
NoteExecutionHint::after_block((u32::MAX - 1).into()).unwrap(),
] {
let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux).unwrap();
let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux);
NoteMetadata::read_from_bytes(&metadata.to_bytes())
.context(format!("failed for execution hint {execution_hint:?}"))?;
}
Expand Down
7 changes: 1 addition & 6 deletions crates/miden-protocol/src/note/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ mod note_id;
pub use note_id::NoteId;

mod note_tag;
pub use note_tag::{NoteExecutionMode, NoteTag};
pub use note_tag::NoteTag;

mod note_type;
pub use note_type::NoteType;
Expand Down Expand Up @@ -156,11 +156,6 @@ impl Note {
pub fn commitment(&self) -> Word {
self.header.commitment()
}

/// Returns true if this note is intended to be executed by the network rather than a user.
pub fn is_network_note(&self) -> bool {
self.metadata().tag().execution_mode() == NoteExecutionMode::Network
}
}

// AS REF
Expand Down
Loading