From f4e13014678f9b687b47b5a452d32ae5465e9618 Mon Sep 17 00:00:00 2001 From: riemann Date: Wed, 7 Jan 2026 09:57:37 -0500 Subject: [PATCH 1/2] feat: implement build_note_tag_for_local_account in masm --- crates/miden-protocol/asm/protocol/note.masm | 68 +++++++++++++++++++ .../src/kernel_tests/tx/test_note.rs | 37 ++++++++++ 2 files changed, 105 insertions(+) diff --git a/crates/miden-protocol/asm/protocol/note.masm b/crates/miden-protocol/asm/protocol/note.masm index 284c68d4ea..d977d9e09c 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -252,3 +252,71 @@ pub proc build_note_tag_for_network_account drop # => [network_account_tag] end + +#! Computes the tag for a local note for a given local account such that it is +#! picked up by the local transaction builder. +#! +#! This procedure implements the same logic as in Rust in NoteTag::from_local_account_id(). +#! Note: This procedure does not check if the account id is a local account id. +#! +#! The tag is constructed as follows: +#! - The two most significant bits are set to `0b11` to indicate a LOCAL_ANY tag. +#! - The next 14 bits are set to the most significant bits of the account ID prefix. +#! - The remaining bits are set to zero. +#! +#! Inputs: [account_id_prefix, account_id_suffix] +#! Outputs: [local_account_tag] +#! +#! Where: +#! - account_id_prefix, account_id_suffix is the account id to compute the note tag for. +#! - local_account_tag is the computed local note tag. +#! +#! Invocation: exec +pub proc build_note_tag_for_local_account + # => [account_id_prefix, account_id_suffix] + + # Drop the suffix as we only need the prefix + swap drop + # => [account_id_prefix] + + # Convert prefix to u64 by splitting into high and low parts + u32split + # => [prefix_hi, prefix_lo] + + # Shift right by 34 bits: prefix >> 34 + # This is equivalent to: (prefix_hi << 32 | prefix_lo) >> 34 + # Which simplifies to: (prefix_hi << (32-34)) | (prefix_lo >> 34) + # Since 32-34 = -2, we need: (prefix_hi >> 2) | (prefix_lo >> 34) + # But prefix_lo >> 34 = 0 (since prefix_lo is u32), so we just need prefix_hi >> 2 + + # Shift prefix_hi right by 2 bits + push.2 + # => [2, prefix_hi, prefix_lo] + + exec.u64::shr + # => [shifted_hi, shifted_lo] + + # Drop the high part since we only need the low 32 bits + swap drop + # => [high_bits] + + # Create mask: u32::MAX << (32 - 2 - 14) = u32::MAX << 16 + push.4294967295 # u32::MAX + push.16 + # => [16, u32::MAX, high_bits] + + # Shift left to create mask + u32shl + # => [mask, high_bits] + + # Apply mask to high_bits + u32and + # => [masked_high_bits] + + # Set the LOCAL_ANY prefix (0xc0000000) by ORing with the masked bits + push.3221225472 # 0xc0000000 = LOCAL_ANY + # => [LOCAL_ANY, masked_high_bits] + + u32or + # => [local_account_tag] +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 8bee856271..a1e3b3d890 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_note.rs @@ -24,6 +24,7 @@ use miden_protocol::note::{ }; use miden_protocol::testing::account_id::{ ACCOUNT_ID_NETWORK_FUNGIBLE_FAUCET, + ACCOUNT_ID_PRIVATE_SENDER, ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_SENDER, }; @@ -594,3 +595,39 @@ async fn test_build_note_tag_for_network_account() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn test_build_note_tag_for_local_account() -> anyhow::Result<()> { + let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; + + let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?; + let expected_tag = NoteTag::from_account_id(account_id).as_u32(); + + let prefix: u64 = account_id.prefix().into(); + let suffix: u64 = account_id.suffix().into(); + + let code = format!( + " + use miden::core::sys + use miden::protocol::note + + begin + push.{suffix}.{prefix} + + exec.note::build_note_tag_for_local_account + # => [local_account_tag] + + exec.sys::truncate_stack + end + ", + suffix = suffix, + prefix = prefix, + ); + + let exec_output = tx_context.execute_code(&code).await?; + let actual_tag = exec_output.stack[0].as_int(); + + assert_eq!(expected_tag, actual_tag as u32,); + + Ok(()) +} From b79f0f168e202fcc1246658f8b88763eabdb8123 Mon Sep 17 00:00:00 2001 From: riemann Date: Wed, 7 Jan 2026 10:02:58 -0500 Subject: [PATCH 2/2] refactor: update masm doc comments & add changelog --- CHANGELOG.md | 1 + crates/miden-protocol/asm/protocol/note.masm | 31 +++++++------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7626770e3..e2751fda42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - [BREAKING] Allowed account components to share identical account code procedures ([#2164](https://github.com/0xMiden/miden-base/pull/2164)). - Add `From<&ExecutedTransaction> for TransactionHeader` implementation ([#2178](https://github.com/0xMiden/miden-base/pull/2178)). - Add `AccountId::parse()` helper function to parse both hex and bech32 formats ([#2223](https://github.com/0xMiden/miden-base/pull/2223)). +- Added `note::build_note_tag_for_local_account` procedure ([#2235](https://github.com/0xMiden/miden-base/pull/2235)). ### Changes diff --git a/crates/miden-protocol/asm/protocol/note.masm b/crates/miden-protocol/asm/protocol/note.masm index d977d9e09c..fda0dcd340 100644 --- a/crates/miden-protocol/asm/protocol/note.masm +++ b/crates/miden-protocol/asm/protocol/note.masm @@ -253,11 +253,10 @@ pub proc build_note_tag_for_network_account # => [network_account_tag] end -#! Computes the tag for a local note for a given local account such that it is -#! picked up by the local transaction builder. +#! Constructs a LocalAny note tag from the given account_id. #! -#! This procedure implements the same logic as in Rust in NoteTag::from_local_account_id(). -#! Note: This procedure does not check if the account id is a local account id. +#! This procedure implements the same logic as the Rust NoteTag::from_local_account_id() +#! but assumes tag_len to be 14 bits. #! #! The tag is constructed as follows: #! - The two most significant bits are set to `0b11` to indicate a LOCAL_ANY tag. @@ -267,10 +266,6 @@ end #! Inputs: [account_id_prefix, account_id_suffix] #! Outputs: [local_account_tag] #! -#! Where: -#! - account_id_prefix, account_id_suffix is the account id to compute the note tag for. -#! - local_account_tag is the computed local note tag. -#! #! Invocation: exec pub proc build_note_tag_for_local_account # => [account_id_prefix, account_id_suffix] @@ -283,37 +278,33 @@ pub proc build_note_tag_for_local_account u32split # => [prefix_hi, prefix_lo] - # Shift right by 34 bits: prefix >> 34 - # This is equivalent to: (prefix_hi << 32 | prefix_lo) >> 34 - # Which simplifies to: (prefix_hi << (32-34)) | (prefix_lo >> 34) - # Since 32-34 = -2, we need: (prefix_hi >> 2) | (prefix_lo >> 34) - # But prefix_lo >> 34 = 0 (since prefix_lo is u32), so we just need prefix_hi >> 2 - - # Shift prefix_hi right by 2 bits + # Shift the high bits of the account ID such that they are laid out as: + # [34 zero bits | remaining high bits (30 bits)] push.2 # => [2, prefix_hi, prefix_lo] exec.u64::shr # => [shifted_hi, shifted_lo] - # Drop the high part since we only need the low 32 bits + # This is equivalent to the following layout, interpreted as a u32: + # [2 zero bits | remaining high bits (30 bits)] swap drop # => [high_bits] + # Select the top 14 bits of the account ID, i.e.: + # [2 zero bits | remaining high bits (14 bits) | (30 - 14) zero bits] # Create mask: u32::MAX << (32 - 2 - 14) = u32::MAX << 16 - push.4294967295 # u32::MAX + push.4294967295 push.16 # => [16, u32::MAX, high_bits] - # Shift left to create mask u32shl # => [mask, high_bits] - # Apply mask to high_bits u32and # => [masked_high_bits] - # Set the LOCAL_ANY prefix (0xc0000000) by ORing with the masked bits + # Set the local execution tag in the two most significant bits push.3221225472 # 0xc0000000 = LOCAL_ANY # => [LOCAL_ANY, masked_high_bits]