Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
68798fc
feat: update note tag length to support up to 32 bits for network acc…
swaploard Jan 22, 2026
b34b414
Merge branch '0xMiden:next' into note-tag-32bit-routing(#2309)
swaploard Jan 22, 2026
56975e4
chore: update changelog for note tag length support up to 32 bits
swaploard Jan 22, 2026
d8e2ba4
Merge branch 'next' into note-tag-32bit-routing(#2309)
bobbinth Jan 23, 2026
504197e
refactor: remove unused NoteTag references and update related logic f…
swaploard Jan 26, 2026
7ffea00
Merge branch '0xMiden:next' into note-tag-32bit-routing(#2309)
swaploard Jan 27, 2026
9f333bd
refactor: update note tag length to support up to 32 bits for network
swaploard Jan 27, 2026
f2e32fe
refactor: simplify changelog entry for note tag length update
swaploard Jan 27, 2026
d47cde3
refactor: clarify note tag construction details for account targeting
swaploard Jan 27, 2026
5c53c17
refactor: update note tag length references to use MAX_ACCOUNT_TARGET…
swaploard Jan 28, 2026
d629ccc
refactor: update assertion for account target tag to check 32 most si…
swaploard Jan 28, 2026
b147ece
refactor: improve documentation for routing parameters attachment in …
swaploard Jan 28, 2026
f68af4e
chore: small improvements
PhilippGackstatter Jan 28, 2026
dee89d8
Merge remote-tracking branch 'origin/next' into note-tag-32bit-routin…
PhilippGackstatter Jan 28, 2026
fabed02
Merge branch 'next' into note-tag-32bit-routing(#2309)
bobbinth Jan 28, 2026
53b3e27
Apply suggestion from @PhilippGackstatter
bobbinth Jan 28, 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 @@ -17,6 +17,7 @@
- [BREAKING] refactored `TransactionAuthenticator::get_public_key()` method to return `Arc<PublicKey> `instead of `&PublicKey` ([#2304](https://github.com/0xMiden/miden-base/pull/2304)).
- [BREAKING] Renamed `NoteInputs` to `NoteStorage` to better reflect that values are stored data associated with a note rather than inputs ([#1662](https://github.com/0xMiden/miden-base/issues/1662), [#2316](https://github.com/0xMiden/miden-base/issues/2316)).
- Removed `NoteType::Encrypted` ([#2315](https://github.com/0xMiden/miden-base/pull/2315)).
- Updated note tag length to support up to 32 bits ([#2329](https://github.com/0xMiden/miden-base/pull/2329)).

## 0.13.3 (2026-01-27)

Expand Down
10 changes: 4 additions & 6 deletions crates/miden-protocol/asm/protocol/note.masm
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,10 @@ pub proc build_note_tag_for_network_account
u32split
# => [a_hi, a_lo]
Comment thread
PhilippGackstatter marked this conversation as resolved.

push.34
# => [b, a_hi, a_lo]
# mask out the 14 (NoteTag::DEFAULT_ACCOUNT_TARGET_TAG_LENGTH) most significant bits
u32and.0xfffc0000
# => [a_hi_masked, a_lo]

exec.u64::shr
# => [c_hi, c_lo]

drop
swap drop
# => [network_account_tag]
end
20 changes: 1 addition & 19 deletions crates/miden-protocol/src/address/address_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ use bech32::Bech32m;
use bech32::primitives::decode::CheckedHrpstring;
use miden_processor::DeserializationError;

use crate::account::{AccountId, AccountStorageMode};
use crate::account::AccountId;
use crate::address::{AddressType, NetworkId};
use crate::errors::{AddressError, Bech32Error};
use crate::note::NoteTag;
use crate::utils::serde::{ByteWriter, Deserializable, Serializable};

/// The identifier of an [`Address`](super::Address).
Expand All @@ -27,23 +26,6 @@ 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_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_ACCOUNT_TARGET_TAG_LENGTH
} else {
NoteTag::DEFAULT_LOCAL_ACCOUNT_TARGET_TAG_LENGTH
}
},
}
}

/// Decodes a bech32 string into an identifier.
pub(crate) fn decode(bech32_string: &str) -> Result<(NetworkId, Self), AddressError> {
// We use CheckedHrpString with an explicit checksum algorithm so we don't allow the
Expand Down
94 changes: 39 additions & 55 deletions crates/miden-protocol/src/address/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
mod r#type;
use alloc::string::ToString;

pub use r#type::AddressType;

Expand All @@ -16,7 +15,6 @@ pub use interface::AddressInterface;
use miden_processor::DeserializationError;
pub use network_id::{CustomNetworkId, NetworkId};

use crate::account::AccountStorageMode;
use crate::crypto::ies::SealingKey;
use crate::errors::AddressError;
use crate::note::NoteTag;
Expand Down Expand Up @@ -79,35 +77,13 @@ impl Address {
Self { id: id.into(), routing_params: None }
}

/// For local (both public and private) accounts, up to 30 bits can be encoded into the tag.
/// For network accounts, the tag length must be set to 30 bits.
///
/// # Errors
///
/// Returns an error if:
/// - 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,
) -> Result<Self, AddressError> {
if let Some(tag_len) = routing_params.note_tag_len() {
match self.id {
AddressId::AccountId(account_id) => {
if account_id.storage_mode() == AccountStorageMode::Network
&& tag_len != NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH
{
return Err(AddressError::CustomTagLengthNotAllowedForNetworkAccounts(
Comment thread
PhilippGackstatter marked this conversation as resolved.
tag_len,
));
}
},
}
}

/// Sets the routing parameters of the address.
/// Validation of tag length, interface, and encryption key is handled
/// internally by [`RoutingParameters`]. This method simply attaches the
/// provided parameters to the address.
pub fn with_routing_parameters(mut self, routing_params: RoutingParameters) -> Self {
Comment thread
PhilippGackstatter marked this conversation as resolved.
self.routing_params = Some(routing_params);

Ok(self)
self
}

// ACCESSORS
Expand All @@ -125,30 +101,25 @@ impl Address {

/// Returns the preferred tag length.
///
/// This is guaranteed to be in range `0..=30` (e.g. the maximum of
/// [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`] and
/// [`NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH`]).
/// This is guaranteed to be in range `0..=32` (e.g. the maximum of
/// [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH `]).
pub fn note_tag_len(&self) -> u8 {
self.routing_params
.as_ref()
.and_then(RoutingParameters::note_tag_len)
.unwrap_or(self.id.default_note_tag_len())
.unwrap_or(NoteTag::DEFAULT_ACCOUNT_TARGET_TAG_LENGTH)
}

/// Returns a note tag derived from this address.
pub fn to_note_tag(&self) -> NoteTag {
let note_tag_len = self.note_tag_len();

match self.id {
AddressId::AccountId(id) => {
match id.storage_mode() {
AccountStorageMode::Network => NoteTag::from_network_account_id(id),
AccountStorageMode::Private | AccountStorageMode::Public => {
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")
}
}
},
AddressId::AccountId(id) => 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 @@ -201,7 +172,7 @@ impl Address {

if let Some(encoded_routing_params) = split.next() {
let routing_params = RoutingParameters::decode(encoded_routing_params.to_owned())?;
address = address.with_routing_parameters(routing_params)?;
address = address.with_routing_parameters(routing_params);
}

Ok((network_id, address))
Expand All @@ -225,9 +196,7 @@ impl Deserializable for Address {
let mut address = Self::new(identifier);

if let Some(routing_params) = routing_params {
address = address
.with_routing_parameters(routing_params)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
address = address.with_routing_parameters(routing_params);
}

Ok(address)
Expand All @@ -246,7 +215,7 @@ mod tests {
use bech32::{Bech32, Bech32m, NoChecksum};

use super::*;
use crate::account::{AccountId, AccountType};
use crate::account::{AccountId, AccountStorageMode, AccountType};
use crate::address::CustomNetworkId;
use crate::errors::{AccountIdError, Bech32Error};
use crate::testing::account_id::{ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, AccountIdBuilder};
Expand Down Expand Up @@ -302,8 +271,8 @@ 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_ACCOUNT_TARGET_TAG_LENGTH)?,
)?;
.with_note_tag_len(NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH)?,
);

let bech32_string = address.encode(network_id.clone());
assert!(
Expand Down Expand Up @@ -346,7 +315,7 @@ mod tests {
let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?;
let address = Address::new(account_id).with_routing_parameters(
RoutingParameters::new(AddressInterface::BasicWallet).with_note_tag_len(14)?,
)?;
);

let bech32_string = address.encode(network_id);
let mut invalid_bech32_1 = bech32_string.clone();
Expand Down Expand Up @@ -431,8 +400,8 @@ 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_ACCOUNT_TARGET_TAG_LENGTH)?,
)?;
.with_note_tag_len(NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH)?,
);

let serialized = address.to_bytes();
let deserialized = Address::read_from_bytes(&serialized)?;
Expand Down Expand Up @@ -463,7 +432,7 @@ mod tests {
let address = Address::new(account_id).with_routing_parameters(
RoutingParameters::new(AddressInterface::BasicWallet)
.with_encryption_key(sealing_key.clone()),
)?;
);

// Verify encryption key is present
let retrieved_key =
Expand Down Expand Up @@ -503,7 +472,7 @@ mod tests {
let address = Address::new(account_id).with_routing_parameters(
RoutingParameters::new(AddressInterface::BasicWallet)
.with_encryption_key(sealing_key.clone()),
)?;
);

// Encode and decode
let encoded = address.encode(NetworkId::Mainnet);
Expand All @@ -521,4 +490,19 @@ mod tests {

Ok(())
}

#[test]
fn address_allows_max_note_tag_len() -> anyhow::Result<()> {
let account_id = AccountIdBuilder::new()
.account_type(AccountType::RegularAccountImmutableCode)
.build_with_rng(&mut rand::rng());

let address = Address::new(account_id).with_routing_parameters(
RoutingParameters::new(AddressInterface::BasicWallet).with_note_tag_len(32)?,
);

assert_eq!(address.note_tag_len(), 32);

Ok(())
}
}
34 changes: 17 additions & 17 deletions crates/miden-protocol/src/address/routing_parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ const BECH32_SEPARATOR: &str = "1";

/// The value to encode the absence of a note tag routing parameter (i.e. `None`).
///
/// The note tag length occupies 5 bits (values 0..=31). Valid tag lengths are 0..=30,
/// so we reserve the maximum 5-bit value (31) to represent `None`.
/// The note tag length occupies 6 bits (values 0..=63). Valid tag lengths are 0..=32,
/// so we reserve the maximum 6-bit value (63) to represent `None`.
///
/// If the note tag length is absent from routing parameters, the note tag length for the address
/// will be set to the default default tag length of the address' ID component.
const ABSENT_NOTE_TAG_LEN: u8 = (1 << 5) - 1; // 31
const ABSENT_NOTE_TAG_LEN: u8 = 63;

/// The routing parameter key for the receiver profile.
const RECEIVER_PROFILE_PARAM_KEY: u8 = 0;
Expand Down Expand Up @@ -92,8 +92,7 @@ impl RoutingParameters {
/// # Errors
///
/// Returns an error if:
/// - The tag length exceeds the maximum of [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`] and
/// [`NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH`].
/// - The tag length exceeds the maximum of [`NoteTag::MAX_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_ACCOUNT_TARGET_TAG_LENGTH {
return Err(AddressError::TagLengthTooLarge(note_tag_len));
Expand All @@ -108,9 +107,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_ACCOUNT_TARGET_TAG_LENGTH`] and
/// [`NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH`]).
/// This is guaranteed to be in range `0..=32` (e.g. the maximum of
/// [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`]).
pub fn note_tag_len(&self) -> Option<u8> {
self.note_tag_len
}
Expand Down Expand Up @@ -269,11 +267,11 @@ fn encode_receiver_profile(interface: AddressInterface, note_tag_len: Option<u8>
let note_tag_len = note_tag_len.unwrap_or(ABSENT_NOTE_TAG_LEN);

let interface = interface as u16;
debug_assert_eq!(interface >> 11, 0, "address interface should have its upper 5 bits unset");
debug_assert_eq!(interface >> 10, 0, "address interface should fit into 10 bits");

// The interface takes up 11 bits and the tag length 5 bits, so we can merge them
// The interface takes up 10 bits and the tag length 6 bits, so we can merge them
// together.
let tag_len = (note_tag_len as u16) << 11;
let tag_len = (note_tag_len as u16) << 10;
let receiver_profile: u16 = tag_len | interface;
receiver_profile.to_be_bytes()
}
Expand All @@ -290,14 +288,16 @@ fn decode_receiver_profile(
let byte1 = byte_iter.next().expect("byte1 should exist");
let receiver_profile = u16::from_be_bytes([byte0, byte1]);

let tag_len = (receiver_profile >> 11) as u8;
let note_tag_len = if tag_len == ABSENT_NOTE_TAG_LEN {
None
} else {
Some(tag_len)
let tag_len = (receiver_profile >> 10) as u8;
let note_tag_len = match tag_len {
ABSENT_NOTE_TAG_LEN => None,
Comment thread
PhilippGackstatter marked this conversation as resolved.
0..=32 => Some(tag_len),
_ => {
return Err(AddressError::decode_error(format!("invalid note tag length {}", tag_len)));
},
};

let addr_interface = receiver_profile & 0b0000_0111_1111_1111;
let addr_interface = receiver_profile & 0b0000_0011_1111_1111;
let addr_interface = AddressInterface::try_from(addr_interface).map_err(|err| {
AddressError::decode_error_with_source("failed to decode address interface", err)
})?;
Expand Down
2 changes: 1 addition & 1 deletion crates/miden-protocol/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ pub enum AccountTreeError {
#[derive(Debug, Error)]
pub enum AddressError {
#[error("tag length {0} should be {expected} bits for network accounts",
expected = NoteTag::DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH
expected = NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH
)]
CustomTagLengthNotAllowedForNetworkAccounts(u8),
#[error("tag length {0} is too large, must be less than or equal to {max}",
Expand Down
Loading