diff --git a/CHANGELOG.md b/CHANGELOG.md index eae7a5205b..1c2f6d9d20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,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) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm index 1da9d7eab2..7e4bb51916 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -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 # ================================================================================================= @@ -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 # ================================================================================================= @@ -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 @@ -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 diff --git a/crates/miden-protocol/asm/protocol/output_note.masm b/crates/miden-protocol/asm/protocol/output_note.masm index 3257be6172..6b1093d0cb 100644 --- a/crates/miden-protocol/asm/protocol/output_note.masm +++ b/crates/miden-protocol/asm/protocol/output_note.masm @@ -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 diff --git a/crates/miden-protocol/src/address/address_id.rs b/crates/miden-protocol/src/address/address_id.rs index 2974e66ac3..7b13ae73c2 100644 --- a/crates/miden-protocol/src/address/address_id.rs +++ b/crates/miden-protocol/src/address/address_id.rs @@ -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 } }, } diff --git a/crates/miden-protocol/src/address/mod.rs b/crates/miden-protocol/src/address/mod.rs index 6e9c864a36..93ef42b210 100644 --- a/crates/miden-protocol/src/address/mod.rs +++ b/crates/miden-protocol/src/address/mod.rs @@ -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, @@ -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, @@ -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() @@ -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") } } }, @@ -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()); @@ -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(); diff --git a/crates/miden-protocol/src/address/routing_parameters.rs b/crates/miden-protocol/src/address/routing_parameters.rs index 5b5899efea..3d9d640ee0 100644 --- a/crates/miden-protocol/src/address/routing_parameters.rs +++ b/crates/miden-protocol/src/address/routing_parameters.rs @@ -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 { - 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)); } @@ -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 { self.note_tag_len } @@ -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(()) } @@ -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(()) } diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 73c2b0688a..e9939ba1aa 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -295,11 +295,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}`")] @@ -541,7 +541,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), @@ -553,8 +553,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, diff --git a/crates/miden-protocol/src/errors/tx_kernel.rs b/crates/miden-protocol/src/errors/tx_kernel.rs index 8394873749..9aaad06249 100644 --- a/crates/miden-protocol/src/errors/tx_kernel.rs +++ b/crates/miden-protocol/src/errors/tx_kernel.rs @@ -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"); diff --git a/crates/miden-protocol/src/note/file.rs b/crates/miden-protocol/src/note/file.rs index 48a77902aa..c00317cc49 100644 --- a/crates/miden-protocol/src/note/file.rs +++ b/crates/miden-protocol/src/note/file.rs @@ -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) } diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index 31cdd14bcd..3705201a3f 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -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: @@ -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 { - let tag = tag.validate(note_type)?; - Ok(Self { + ) -> Self { + Self { sender, note_type, tag, aux, execution_hint, - }) + } } /// Returns the account which created the note. @@ -162,7 +153,7 @@ impl TryFrom 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])) } } @@ -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 [ @@ -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:?}"))?; } diff --git a/crates/miden-protocol/src/note/mod.rs b/crates/miden-protocol/src/note/mod.rs index 1ecf14bfa9..a87252bcbf 100644 --- a/crates/miden-protocol/src/note/mod.rs +++ b/crates/miden-protocol/src/note/mod.rs @@ -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; @@ -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 diff --git a/crates/miden-protocol/src/note/note_tag.rs b/crates/miden-protocol/src/note/note_tag.rs index 4da8f012fb..3c1ee2242a 100644 --- a/crates/miden-protocol/src/note/note_tag.rs +++ b/crates/miden-protocol/src/note/note_tag.rs @@ -9,163 +9,117 @@ use super::{ Deserializable, DeserializationError, NoteError, - NoteType, Serializable, }; use crate::account::AccountStorageMode; -// CONSTANTS -// ================================================================================================ -const NETWORK_EXECUTION: u8 = 0; -const LOCAL_EXECUTION: u8 = 1; - -// The 2 most significant bits are set to `0b00`. -const NETWORK_ACCOUNT: u32 = 0; -// The 2 most significant bits are set to `0b01`. -const NETWORK_PUBLIC_USECASE: u32 = 0x4000_0000; -// The 2 most significant bits are set to `0b10`. -const LOCAL_PUBLIC_ANY: u32 = 0x8000_0000; -// The 2 most significant bits are set to `0b11`. -const LOCAL_ANY: u32 = 0xc000_0000; - -/// [super::Note]'s execution mode hints. -/// -/// The execution hints are _not_ enforced, therefore function only as hints. For example, if a -/// note's tag is created with the [NoteExecutionMode::Network], further validation is necessary to -/// check the account_id is known, that the account's state is public on chain, and the account is -/// controlled by the network. -/// -/// The goal of the hint is to allow for a network node to quickly filter notes that are not -/// intended for network execution, and skip the validation steps mentioned above. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum NoteExecutionMode { - Network = NETWORK_EXECUTION, - Local = LOCAL_EXECUTION, -} - // NOTE TAG // ================================================================================================ -/// [NoteTag]`s are best effort filters for notes registered with the network. +/// [`NoteTag`]s are 32-bits of data that serve as best-effort filters for notes. /// -/// Tags are light-weight values used to speed up queries. The 2 most significant bits of the tags -/// have the following interpretation: +/// Tags enable quick lookups for notes related to particular use cases, scripts, or account +/// prefixes. /// -/// | 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 | +/// ## Account Targets /// -/// Where: +/// A note targeted at an account is a note that is intended or even enforced to be consumed by a +/// specific account. One example is a P2ID note that can only be consumed by a specific account ID. +/// The tag for such a note should make it easy for the receiver to find the note. Therefore, the +/// tag encodes a certain number of bits of the receiver account's ID, by convention. Notably, it +/// may not encode the full 32 bits of the target account's ID to preserve the receiver's privacy. +/// See also the section on privacy below. /// -/// - [`NoteExecutionMode`] is set to [`NoteExecutionMode::Network`] to hint a [`Note`](super::Note) -/// should be consumed by the network. These notes will be further validated and if possible -/// consumed by it. -/// - Target describes how to further interpret the bits in the tag. -/// - For tags with a specific target, the rest of the tag is interpreted as a partial -/// [`AccountId`]. For network accounts these are the first 30 bits of the ID while for local -/// account targets, the first 14 bits are used - a trade-off between privacy and uniqueness. -/// - For use case values, the meaning of the rest of the tag is not specified by the protocol and -/// can be used by applications built on top of the rollup. +/// Because this convention is widely used, the note tag provides a dedicated constructor for this: +/// [`NoteTag::with_account_target`]. /// -/// The note type is the only value enforced by the protocol. The rationale is that any note -/// intended to be consumed by the network must be public to have all the details available. The -/// public note for local execution is intended to allow users to search for notes that can be -/// consumed right away, without requiring an off-band communication channel. +/// ## Use Case Tags /// -/// **Note on Type Safety** +/// Use case notes are notes that are not intended to be consumed by a specific account, but by +/// anyone willing to fulfill the note's contract. One example is a SWAP note that trades one asset +/// against another. Such a use case note can define the structure of their note tags. A sensible +/// structure for a SWAP note could be: +/// - encoding the 2 bits of the note's type. +/// - encoding the note script root, i.e. making it identifiable as a SWAP note, for example by +/// using 16 bits of the SWAP script root. +/// - encoding the SWAP pair, for example by using 8 bits of the offered asset faucet ID and 8 bits +/// of the requested asset faucet ID. /// -/// Each enum variant contains the raw encoding of the note tag, where the first two bits -/// _should_ correspond to the variant's prefix (as defined in the table above). However, because -/// enum variants are always public, it is possible to instantiate this enum where this invariant -/// does not hold, e.g. `NoteTag::NetworkAccount(0b11...)`. For that reason, the enum variants -/// should take precedence in case of such a mismatch and the inner value **should not be accessed -/// directly**. Instead, only rely on [`NoteTag::as_u32`] to access the encoded value, which will -/// always return the correct value. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum NoteTag { - /// Represents a tag for a note intended for network execution, targeted at a network account. - /// The note must be public. - NetworkAccount(u32), - /// Represents a tag for a note intended for network execution for a public use case. The note - /// must be public. - NetworkUseCase(u16, u16), - /// Represents a tag for a note intended for local execution. - /// - /// This is used for two purposes: - /// - A public use case. - /// - A note targeted at any type of account. - /// - /// In all cases, the note must be **public**. - LocalPublicAny(u32), - /// Represents a tag for a note intended for local execution. - /// - /// This is used for two purposes: - /// - A private use case. - /// - A note targeted at any type of account. - /// - /// In all cases, the note can be of any type. - LocalAny(u32), -} +/// This allows clients to search for a public SWAP note that trades USDC against ETH only through +/// the note tag. Since tags are not validated in any way and only act as best-effort filters, +/// further local filtering is almost always necessary. For example, there could easily be a +/// collision on the 8 bits used in SWAP tag's faucet IDs. +/// +/// ## Privacy vs Efficiency +/// +/// Using note tags strikes a balance between privacy and efficiency. Without tags, querying a +/// specific note ID reveals a user's interest to the node. Conversely, downloading and filtering +/// all registered notes locally is highly inefficient. Tags allow users to adjust their level of +/// privacy by choosing how broadly or narrowly they define their search criteria, letting them find +/// the right balance between revealing too much information and incurring excessive computational +/// overhead. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Default)] +pub struct NoteTag(u32); impl NoteTag { // CONSTANTS // -------------------------------------------------------------------------------------------- - /// The exponent of the maximum allowed use case id. In other words, 2^exponent is the maximum - /// allowed use case id. - pub(crate) const MAX_USE_CASE_ID_EXPONENT: u8 = 14; /// The default note tag length for an account ID with local execution. - pub const DEFAULT_LOCAL_TAG_LENGTH: u8 = 14; + pub const DEFAULT_LOCAL_ACCOUNT_TARGET_TAG_LENGTH: u8 = 14; /// The default note tag length for an account ID with network execution. - pub const DEFAULT_NETWORK_TAG_LENGTH: u8 = 30; + pub const DEFAULT_NETWORK_ACCOUNT_TARGET_TAG_LENGTH: u8 = 30; /// The maximum number of bits that can be encoded into the tag for local accounts. - pub const MAX_LOCAL_TAG_LENGTH: u8 = 30; + pub const MAX_ACCOUNT_TARGET_TAG_LENGTH: u8 = 30; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Returns a new [NoteTag::NetworkAccount] or [NoteTag::LocalAny] instantiated from the - /// specified account ID. + /// Creates a new [`NoteTag`] from an arbitrary `u32`. + pub const fn new(tag: u32) -> Self { + Self(tag) + } + + /// Constructs a note tag that targets the given `account_id`. /// /// The tag is constructed as follows: /// /// - For local execution ([`AccountStorageMode::Private`] or [`AccountStorageMode::Public`]), - /// the two most significant bits are set to `0b11`, which allows for any note type to be - /// used. The following 14 bits are set to the most significant bits of the account ID, and - /// the remaining 16 bits are set to 0. + /// the two most significant bits are set to `0b00`. The following 14 bits are set to the most + /// significant bits of the account ID, and the remaining 16 bits are set to 0. /// - For network execution ([`AccountStorageMode::Network`]), the most significant bits are set /// to `0b00` and the remaining bits are set to the 30 most significant bits of the account /// ID. - pub fn from_account_id(account_id: AccountId) -> Self { + pub fn with_account_target(account_id: AccountId) -> Self { match account_id.storage_mode() { AccountStorageMode::Network => Self::from_network_account_id(account_id), AccountStorageMode::Private | AccountStorageMode::Public => { - // safe to unwrap since DEFAULT_LOCAL_TAG_LENGTH < MAX_LOCAL_TAG_LENGTH - Self::from_local_account_id(account_id, Self::DEFAULT_LOCAL_TAG_LENGTH).unwrap() + // safe to unwrap since DEFAULT_LOCAL_ACCOUNT_TARGET_TAG_LENGTH < + // MAX_ACCOUNT_TARGET_TAG_LENGTH + Self::with_custom_account_target( + account_id, + Self::DEFAULT_LOCAL_ACCOUNT_TARGET_TAG_LENGTH, + ) + .unwrap() }, } } - /// Constructs a [`NoteTag::LocalAny`] from the given `account_id` and `tag_len`. + /// Constructs a note tag that targets the given `account_id` with a custom `tag_len`. /// - /// The tag is constructed as follows: - /// - /// - The two most significant bits are set to `0b11` to indicate a [LOCAL_ANY] tag. + /// The tag is constructed by: + /// - Setting the two most significant bits to zero. /// - The next `tag_len` bits are set to the most significant bits of the account ID prefix. /// - The remaining bits are set to zero. /// /// # Errors /// - /// Returns an error if `tag_len` is larger than [`NoteTag::MAX_LOCAL_TAG_LENGTH`]. - pub(crate) fn from_local_account_id( + /// Returns an error if `tag_len` is larger than [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`]. + pub fn with_custom_account_target( account_id: AccountId, tag_len: u8, ) -> Result { - if tag_len > Self::MAX_LOCAL_TAG_LENGTH { + if tag_len > Self::MAX_ACCOUNT_TARGET_TAG_LENGTH { return Err(NoteError::NoteTagLengthTooLarge(tag_len)); } @@ -183,15 +137,14 @@ impl NoteTag { // [2 zero bits | remaining high bits (tag_len bits) | (30 - tag_len) zero bits]. let high_bits = high_bits & (u32::MAX << (32 - 2 - tag_len)); - // Set the local execution tag in the two most significant bits. - Ok(Self::LocalAny(LOCAL_ANY | high_bits)) + Ok(Self(high_bits)) } - /// Constructs a [`NoteTag::NetworkAccount`] from the specified `account_id`. + /// Constructs a network account note tag from the specified `account_id`. /// /// The tag is constructed as follows: /// - /// - The two most significant bits are set to `0b00` to indicate a [NETWORK_ACCOUNT] tag. + /// - The two most significant bits are set to `0b00`. /// - The remaining bits are set to the 30 most significant bits of the account ID. pub(crate) fn from_network_account_id(account_id: AccountId) -> Self { let prefix_id: u64 = account_id.prefix().into(); @@ -202,125 +155,15 @@ impl NoteTag { // This is equivalent to the following layout, interpreted as a u32: // [2 zero bits | remaining high bits (30 bits)]. - // The two most significant zero bits match the tag we need for network - Self::NetworkAccount(high_bits as u32) - } - - /// Returns a new [`NoteTag::NetworkUseCase`] or [`NoteTag::LocalPublicAny`] - /// instantiated for a custom use case which requires a public note. - /// - /// The public use_case tag requires a [NoteType::Public] note. - /// - /// The two high bits are set to the `b10` or `b01` depending on the execution hint, the next 14 - /// bits are set to the `use_case_id`, and the low 16 bits are set to `payload`. - /// - /// # Errors - /// - /// - If `use_case_id` is larger than or equal to $2^{14}$. - pub fn for_public_use_case( - use_case_id: u16, - payload: u16, - execution: NoteExecutionMode, - ) -> Result { - if (use_case_id >> 14) != 0 { - return Err(NoteError::NoteTagUseCaseTooLarge(use_case_id)); - } - - match execution { - NoteExecutionMode::Network => { - let use_case_bits = (NETWORK_PUBLIC_USECASE >> 16) as u16 | use_case_id; - Ok(Self::NetworkUseCase(use_case_bits, payload)) - }, - NoteExecutionMode::Local => { - let tag_u32 = LOCAL_PUBLIC_ANY | ((use_case_id as u32) << 16) | (payload as u32); - Ok(Self::LocalPublicAny(tag_u32)) - }, - } - } - - /// Returns a new [`NoteTag::LocalAny`] instantiated for a custom local use case. - /// - /// The local use_case tag is the only tag type that allows for [NoteType::Private] notes. - /// - /// The two high bits are set to the `b11`, the next 14 bits are set to the `use_case_id`, and - /// the low 16 bits are set to `payload`. - /// - /// # Errors - /// - /// - If `use_case_id` is larger than or equal to 2^14. - pub fn for_local_use_case(use_case_id: u16, payload: u16) -> Result { - if (use_case_id >> NoteTag::MAX_USE_CASE_ID_EXPONENT) != 0 { - return Err(NoteError::NoteTagUseCaseTooLarge(use_case_id)); - } - - let use_case_bits = (use_case_id as u32) << 16; - let payload_bits = payload as u32; - - Ok(Self::LocalAny(LOCAL_ANY | use_case_bits | payload_bits)) + Self(high_bits as u32) } // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns true if the note is intended for execution by a specific account, i.e. - /// [`NoteTag::NetworkAccount`] - pub fn is_single_target(&self) -> bool { - matches!(self, NoteTag::NetworkAccount(_)) - } - - /// Returns note execution mode defined by this tag. - /// - /// If the most significant bit of the tag is 0 the note is intended for local execution; - /// otherwise, the note is intended for network execution. - pub fn execution_mode(&self) -> NoteExecutionMode { - match self { - NoteTag::NetworkAccount(_) | NoteTag::NetworkUseCase(..) => NoteExecutionMode::Network, - NoteTag::LocalAny(_) | NoteTag::LocalPublicAny(..) => NoteExecutionMode::Local, - } - } - /// Returns the inner u32 value of this tag. pub fn as_u32(&self) -> u32 { - // Note that we always set the two most significant bits to the prefix corresponding to the - // enum variant. See the note on type safety on the NoteTag docs. - - /// Masks out the two most significant bits, leaving all lower bits untouched. - const LOW_BITS_MASK: u32 = 0x3fff_ffff; - match self { - NoteTag::NetworkAccount(tag) => *tag & LOW_BITS_MASK, - NoteTag::NetworkUseCase(use_case_bits, payload_bits) => { - ((*use_case_bits as u32) << 16 | *payload_bits as u32) & LOW_BITS_MASK - | NETWORK_PUBLIC_USECASE - }, - NoteTag::LocalPublicAny(tag) => (*tag & LOW_BITS_MASK) | LOCAL_PUBLIC_ANY, - NoteTag::LocalAny(tag) => (*tag & LOW_BITS_MASK) | LOCAL_ANY, - } - } - - // UTILITY METHODS - // -------------------------------------------------------------------------------------------- - - /// Returns an error if this tag is not consistent with the specified note type, and self - /// otherwise. - pub fn validate(&self, note_type: NoteType) -> Result { - if self.execution_mode() == NoteExecutionMode::Network && note_type != NoteType::Public { - return Err(NoteError::NetworkExecutionRequiresPublicNote(note_type)); - } - - // Ensure the note is public if the note tag requires it. - if self.requires_public_note() && note_type != NoteType::Public { - Err(NoteError::PublicNoteRequired(note_type)) - } else { - Ok(*self) - } - } - - /// Returns `true` if the note tag requires a public note. - fn requires_public_note(&self) -> bool { - matches!( - self, - NoteTag::NetworkAccount(_) | NoteTag::NetworkUseCase(_, _) | NoteTag::LocalPublicAny(_) - ) + self.0 } } @@ -334,19 +177,8 @@ impl fmt::Display for NoteTag { // ================================================================================================ impl From for NoteTag { - fn from(value: u32) -> Self { - // Mask out the two most significant bits. - match value & 0xc0000000 { - NETWORK_ACCOUNT => Self::NetworkAccount(value), - NETWORK_PUBLIC_USECASE => Self::NetworkUseCase((value >> 16) as u16, value as u16), - LOCAL_ANY => Self::LocalAny(value), - LOCAL_PUBLIC_ANY => Self::LocalPublicAny(value), - _ => { - // This branch should be unreachable because `prefix` is derived from - // the top 2 bits of a u32, which can only be 0, 1, 2, or 3. - unreachable!("Invalid value encountered: {:b}", value); - }, - } + fn from(tag: u32) -> Self { + Self::new(tag) } } @@ -354,14 +186,14 @@ impl From for NoteTag { // ================================================================================================ impl From for u32 { - fn from(value: NoteTag) -> Self { - value.as_u32() + fn from(tag: NoteTag) -> Self { + tag.as_u32() } } impl From for Felt { - fn from(value: NoteTag) -> Self { - Felt::from(value.as_u32()) + fn from(tag: NoteTag) -> Self { + Felt::from(tag.as_u32()) } } @@ -377,7 +209,7 @@ impl Serializable for NoteTag { impl Deserializable for NoteTag { fn read_from(source: &mut R) -> Result { let tag = u32::read_from(source)?; - Ok(Self::from(tag)) + Ok(Self::new(tag)) } } @@ -387,18 +219,8 @@ impl Deserializable for NoteTag { #[cfg(test)] mod tests { - use assert_matches::assert_matches; - - use super::{NoteExecutionMode, NoteTag}; - use crate::NoteError; + use super::NoteTag; use crate::account::AccountId; - use crate::note::NoteType; - use crate::note::note_tag::{ - LOCAL_ANY, - LOCAL_PUBLIC_ANY, - NETWORK_ACCOUNT, - NETWORK_PUBLIC_USECASE, - }; use crate::testing::account_id::{ ACCOUNT_ID_NETWORK_FUNGIBLE_FAUCET, ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET, @@ -448,223 +270,43 @@ mod tests { AccountId::try_from(ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET).unwrap(), ]; - for account_id in network_accounts { - let tag = NoteTag::from_account_id(account_id); - assert!(tag.is_single_target()); - assert_eq!(tag.execution_mode(), NoteExecutionMode::Network); - - tag.validate(NoteType::Public) - .expect("network execution should require notes to be public"); - assert_matches!( - tag.validate(NoteType::Private), - Err(NoteError::NetworkExecutionRequiresPublicNote(NoteType::Private)) + for account_id in private_accounts.iter().chain(public_accounts.iter()) { + let tag = NoteTag::with_account_target(*account_id); + assert_eq!(tag.as_u32() >> 30, 0, "two most significant bits should be zero"); + assert_eq!(tag.as_u32() << 16, 0, "16 least significant bits should be zero"); + assert_eq!( + (account_id.prefix().as_u64() >> 50) as u32, + tag.as_u32() >> 16, + "14 most significant bits should match" ); - assert_matches!( - tag.validate(NoteType::Encrypted), - Err(NoteError::NetworkExecutionRequiresPublicNote(NoteType::Encrypted)) - ); - } - - for account_id in private_accounts { - let tag = NoteTag::from_account_id(account_id); - assert!(!tag.is_single_target()); - assert_eq!(tag.execution_mode(), NoteExecutionMode::Local); - - // for local execution[`NoteExecutionMode::Local`], all notes are allowed - tag.validate(NoteType::Public) - .expect("local execution should support public notes"); - tag.validate(NoteType::Private) - .expect("local execution should support private notes"); - tag.validate(NoteType::Encrypted) - .expect("local execution should support encrypted notes"); - } - - for account_id in public_accounts { - let tag = NoteTag::from_account_id(account_id); - assert!(!tag.is_single_target()); - assert_eq!(tag.execution_mode(), NoteExecutionMode::Local); - - // for local execution[`NoteExecutionMode::Local`], all notes are allowed - tag.validate(NoteType::Public) - .expect("local execution should support public notes"); - tag.validate(NoteType::Private) - .expect("local execution should support private notes"); - tag.validate(NoteType::Encrypted) - .expect("local execution should support encrypted notes"); } for account_id in network_accounts { - let tag = NoteTag::from_account_id(account_id); - assert!(tag.is_single_target()); - assert_eq!(tag.execution_mode(), NoteExecutionMode::Network); - - // for network execution[`NoteExecutionMode::Network`], only public notes are allowed - tag.validate(NoteType::Public) - .expect("network execution should support public notes"); - assert_matches!( - tag.validate(NoteType::Private), - Err(NoteError::NetworkExecutionRequiresPublicNote(NoteType::Private)) - ); - assert_matches!( - tag.validate(NoteType::Encrypted), - Err(NoteError::NetworkExecutionRequiresPublicNote(NoteType::Encrypted)) + let tag = NoteTag::with_account_target(account_id); + assert_eq!(tag.as_u32() >> 30, 0, "two most significant bits should be zero"); + assert_eq!( + account_id.prefix().as_u64() >> 34, + tag.as_u32() as u64, + "30 most significant bits should match" ); } } #[test] - fn from_private_account_id() { - /// Private Account ID with the following bit pattern in the first and second byte: - /// 0b11001100_01010101 - /// ^^^^^^^^ ^^^^^^ <- 14 bits of the local tag. - const PRIVATE_ACCOUNT_INT: u128 = ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE - | 0x0055_0000_0000_0000_0000_0000_0000_0000; - let private_account_id = AccountId::try_from(PRIVATE_ACCOUNT_INT).unwrap(); - - // Expected private tag of variant `NoteTag::LocalAny`. - let expected_private_tag = 0b11110011_00010101_00000000_00000000; - - assert_eq!(NoteTag::from_account_id(private_account_id).as_u32(), expected_private_tag); - } - - #[test] - fn from_public_account_id() { - /// Public Account ID with the following bit pattern in the first and second byte: - /// 0b10101010_01010101 - /// ^^^^^^^^ ^^^^^^ <- 14 bits of the local tag. - const PUBLIC_ACCOUNT_INT: u128 = ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE - | 0x0055_ccaa_0000_0000_0000_0000_0000_0000; - let public_account_id = AccountId::try_from(PUBLIC_ACCOUNT_INT).unwrap(); - - // Expected public tag of variant `NoteTag::LocalAny`. - let expected_public_local_tag = 0b11101010_10010101_00000000_00000000u32; - - assert_eq!(NoteTag::from_account_id(public_account_id).as_u32(), expected_public_local_tag); - } - - #[test] - fn from_network_account_id() { - /// Network Account ID with the following bit pattern in the first four bytes: - /// 0b10101010_11001100_01110111_11001100 - /// ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^ <- 30 bits of the network tag. - const NETWORK_ACCOUNT_INT: u128 = ACCOUNT_ID_REGULAR_NETWORK_ACCOUNT_IMMUTABLE_CODE - | 0x00cc_77cc_0000_0000_0000_0000_0000_0000; - let network_account_id = AccountId::try_from(NETWORK_ACCOUNT_INT).unwrap(); - - // Expected network tag of variant `NoteTag::NetworkAccount`. - let expected_network_tag = 0b00101010_10110011_00011101_11110011; - - assert_eq!(NoteTag::from_account_id(network_account_id).as_u32(), expected_network_tag); - } - - #[test] - fn for_public_use_case() { - // NETWORK - // ---------------------------------------------------------------------------------------- - let tag = NoteTag::for_public_use_case(0b0, 0b0, NoteExecutionMode::Network).unwrap(); - assert_eq!(tag.as_u32(), 0b01000000_00000000_00000000_00000000u32); - - tag.validate(NoteType::Public).unwrap(); - - assert_matches!( - tag.validate(NoteType::Private).unwrap_err(), - NoteError::NetworkExecutionRequiresPublicNote(NoteType::Private) - ); - assert_matches!( - tag.validate(NoteType::Encrypted).unwrap_err(), - NoteError::NetworkExecutionRequiresPublicNote(NoteType::Encrypted) - ); - - let tag = NoteTag::for_public_use_case(0b1, 0b0, NoteExecutionMode::Network).unwrap(); - assert_eq!(tag.as_u32(), 0b01000000_00000001_00000000_00000000u32); - - let tag = NoteTag::for_public_use_case(0b0, 0b1, NoteExecutionMode::Network).unwrap(); - assert_eq!(tag.as_u32(), 0b01000000_00000000_00000000_00000001u32); - - let tag = NoteTag::for_public_use_case(1 << 13, 0b0, NoteExecutionMode::Network).unwrap(); - assert_eq!(tag.as_u32(), 0b01100000_00000000_00000000_00000000u32); - - // LOCAL - // ---------------------------------------------------------------------------------------- - let tag = NoteTag::for_public_use_case(0b0, 0b0, NoteExecutionMode::Local).unwrap(); - assert_eq!(tag.as_u32(), 0b10000000_00000000_00000000_00000000u32); - - tag.validate(NoteType::Public).unwrap(); - assert_matches!( - tag.validate(NoteType::Private).unwrap_err(), - NoteError::PublicNoteRequired(NoteType::Private) - ); - assert_matches!( - tag.validate(NoteType::Encrypted).unwrap_err(), - NoteError::PublicNoteRequired(NoteType::Encrypted) - ); - - let tag = NoteTag::for_public_use_case(0b0, 0b1, NoteExecutionMode::Local).unwrap(); - assert_eq!(tag.as_u32(), 0b10000000_00000000_00000000_00000001u32); - - let tag = NoteTag::for_public_use_case(0b1, 0b0, NoteExecutionMode::Local).unwrap(); - assert_eq!(tag.as_u32(), 0b10000000_00000001_00000000_00000000u32); - - let tag = NoteTag::for_public_use_case(1 << 13, 0b0, NoteExecutionMode::Local).unwrap(); - assert_eq!(tag.as_u32(), 0b10100000_00000000_00000000_00000000u32); - - assert_matches!( - NoteTag::for_public_use_case(1 << 15, 0b0, NoteExecutionMode::Local).unwrap_err(), - NoteError::NoteTagUseCaseTooLarge(use_case) if use_case == 1 << 15 - ); - assert_matches!( - NoteTag::for_public_use_case(1 << 14, 0b0, NoteExecutionMode::Local).unwrap_err(), - NoteError::NoteTagUseCaseTooLarge(use_case) if use_case == 1 << 14 - ); - } - - #[test] - fn for_private_use_case() { - let tag = NoteTag::for_local_use_case(0b0, 0b0).unwrap(); + fn from_custom_account_target() -> anyhow::Result<()> { + let account_id = AccountId::try_from(ACCOUNT_ID_SENDER)?; + let tag = NoteTag::with_custom_account_target( + account_id, + NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH, + )?; + + assert_eq!(tag.as_u32() >> 30, 0, "two most significant bits should be zero"); assert_eq!( - tag.as_u32() >> 30, - LOCAL_ANY >> 30, - "local use case prefix should be local any" + (account_id.prefix().as_u64() >> 34) as u32, + tag.as_u32(), + "30 most significant bits should match" ); - assert_eq!(tag.as_u32(), 0b11000000_00000000_00000000_00000000u32); - - tag.validate(NoteType::Public) - .expect("local execution should support public notes"); - tag.validate(NoteType::Private) - .expect("local execution should support private notes"); - tag.validate(NoteType::Encrypted) - .expect("local execution should support encrypted notes"); - - let tag = NoteTag::for_local_use_case(0b0, 0b1).unwrap(); - assert_eq!(tag.as_u32(), 0b11000000_00000000_00000000_00000001u32); - - let tag = NoteTag::for_local_use_case(0b1, 0b0).unwrap(); - assert_eq!(tag.as_u32(), 0b11000000_00000001_00000000_00000000u32); - let tag = NoteTag::for_local_use_case(1 << 13, 0b0).unwrap(); - assert_eq!(tag.as_u32(), 0b11100000_00000000_00000000_00000000u32); - - assert_matches!( - NoteTag::for_local_use_case(1 << 15, 0b0).unwrap_err(), - NoteError::NoteTagUseCaseTooLarge(use_case) if use_case == 1 << 15 - ); - assert_matches!( - NoteTag::for_local_use_case(1 << 14, 0b0).unwrap_err(), - NoteError::NoteTagUseCaseTooLarge(use_case) if use_case == 1 << 14 - ); - } - - /// Tests that as_u32 returns the correct prefix independent of the inner value. - #[test] - fn note_tag_as_u32() { - const HIGH_BITS_MASK: u32 = 0xc000_0000; - - assert_eq!(NoteTag::NetworkAccount(u32::MAX).as_u32() & HIGH_BITS_MASK, NETWORK_ACCOUNT); - assert_eq!( - NoteTag::NetworkUseCase(u16::MAX, u16::MAX).as_u32() & HIGH_BITS_MASK, - NETWORK_PUBLIC_USECASE - ); - assert_eq!(NoteTag::LocalPublicAny(u32::MAX).as_u32() & HIGH_BITS_MASK, LOCAL_PUBLIC_ANY); - assert_eq!(NoteTag::LocalAny(0).as_u32() & HIGH_BITS_MASK, LOCAL_ANY); + Ok(()) } } diff --git a/crates/miden-protocol/src/testing/mock_util_lib.rs b/crates/miden-protocol/src/testing/mock_util_lib.rs index 9337fde0f5..b319d2982c 100644 --- a/crates/miden-protocol/src/testing/mock_util_lib.rs +++ b/crates/miden-protocol/src/testing/mock_util_lib.rs @@ -7,9 +7,9 @@ use crate::utils::sync::LazyLock; const MOCK_UTIL_LIBRARY_CODE: &str = " use miden::protocol::output_note - # Inputs: [] - # Outputs: [note_idx] - pub proc create_random_note + #! Inputs: [] + #! Outputs: [note_idx] + pub proc create_default_note push.1.2.3.4 # = RECIPIENT push.1 # = NoteExecutionHint::Always push.2 # = NoteType::Private @@ -21,10 +21,10 @@ const MOCK_UTIL_LIBRARY_CODE: &str = " # => [note_idx] end - # Inputs: [ASSET] - # Outputs: [] - pub proc create_random_note_with_asset - exec.create_random_note + #! Inputs: [ASSET] + #! Outputs: [] + pub proc create_default_note_with_asset + exec.create_default_note # => [note_idx, ASSET] movdn.4 diff --git a/crates/miden-protocol/src/testing/note.rs b/crates/miden-protocol/src/testing/note.rs index 1ffd6c8168..13c8a9464e 100644 --- a/crates/miden-protocol/src/testing/note.rs +++ b/crates/miden-protocol/src/testing/note.rs @@ -28,11 +28,10 @@ impl Note { let metadata = NoteMetadata::new( sender_id, NoteType::Private, - NoteTag::from_account_id(sender_id), + NoteTag::with_account_target(sender_id), NoteExecutionHint::Always, ZERO, - ) - .unwrap(); + ); let inputs = NoteInputs::new(Vec::new()).unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, inputs); diff --git a/crates/miden-protocol/src/transaction/kernel/procedures.rs b/crates/miden-protocol/src/transaction/kernel/procedures.rs index ad580b495f..e32f63b8dc 100644 --- a/crates/miden-protocol/src/transaction/kernel/procedures.rs +++ b/crates/miden-protocol/src/transaction/kernel/procedures.rs @@ -80,7 +80,7 @@ pub const KERNEL_PROCEDURES: [Word; 52] = [ // input_note_get_recipient word!("0xd3c255177f9243bb1a523a87615bbe76dd5a3605fcae87eb9d3a626d4ecce33c"), // output_note_create - word!("0x6562a9d1d8605d158415a4dcd4c8fd48fc2b6c21ec64c188db9def16b991a560"), + word!("0xa40e074cd5bd330b0af04c55b839da1e60e72583136f45d3e1bbc8b847865c3a"), // output_note_get_metadata word!("0xde4a5b57f9d53692459383e6cf6302ef3602a348896ed6ab6fdf67e07fa483ff"), // output_note_get_assets_info diff --git a/crates/miden-standards/src/account/interface/test.rs b/crates/miden-standards/src/account/interface/test.rs index 5c86dedab7..5b63a933ad 100644 --- a/crates/miden-standards/src/account/interface/test.rs +++ b/crates/miden-standards/src/account/interface/test.rs @@ -251,15 +251,14 @@ fn test_basic_wallet_custom_notes() { let sender_account_id = ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2.try_into().unwrap(); let serial_num = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); - let tag = NoteTag::from_account_id(wallet_account.id()); + let tag = NoteTag::with_account_target(wallet_account.id()); let metadata = NoteMetadata::new( sender_account_id, NoteType::Public, tag, NoteExecutionHint::always(), Default::default(), - ) - .unwrap(); + ); let vault = NoteAssets::new(vec![FungibleAsset::mock(100)]).unwrap(); let compatible_source_code = " @@ -341,15 +340,14 @@ fn test_basic_fungible_faucet_custom_notes() { let sender_account_id = ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2.try_into().unwrap(); let serial_num = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); - let tag = NoteTag::from_account_id(faucet_account.id()); + let tag = NoteTag::with_account_target(faucet_account.id()); let metadata = NoteMetadata::new( sender_account_id, NoteType::Public, tag, NoteExecutionHint::always(), Default::default(), - ) - .unwrap(); + ); let vault = NoteAssets::new(vec![FungibleAsset::mock(100)]).unwrap(); let compatible_source_code = " @@ -451,15 +449,14 @@ fn test_custom_account_custom_notes() { .expect("failed to create wallet account"); let serial_num = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); - let tag = NoteTag::from_account_id(target_account.id()); + let tag = NoteTag::with_account_target(target_account.id()); let metadata = NoteMetadata::new( sender_account.id(), NoteType::Public, tag, NoteExecutionHint::always(), Default::default(), - ) - .unwrap(); + ); let vault = NoteAssets::new(vec![FungibleAsset::mock(100)]).unwrap(); let compatible_source_code = " @@ -562,15 +559,14 @@ fn test_custom_account_multiple_components_custom_notes() { .expect("failed to create wallet account"); let serial_num = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); - let tag = NoteTag::from_account_id(target_account.id()); + let tag = NoteTag::with_account_target(target_account.id()); let metadata = NoteMetadata::new( sender_account.id(), NoteType::Public, tag, NoteExecutionHint::always(), Default::default(), - ) - .unwrap(); + ); let vault = NoteAssets::new(vec![FungibleAsset::mock(100)]).unwrap(); let compatible_source_code = " diff --git a/crates/miden-standards/src/note/mod.rs b/crates/miden-standards/src/note/mod.rs index 2dbf1db2a6..1ad2c012ae 100644 --- a/crates/miden-standards/src/note/mod.rs +++ b/crates/miden-standards/src/note/mod.rs @@ -49,9 +49,9 @@ pub fn create_p2id_note( let serial_num = rng.draw_word(); let recipient = utils::build_p2id_recipient(target, serial_num)?; - let tag = NoteTag::from_account_id(target); + let tag = NoteTag::with_account_target(target); - let metadata = NoteMetadata::new(sender, note_type, tag, NoteExecutionHint::always(), aux)?; + let metadata = NoteMetadata::new(sender, note_type, tag, NoteExecutionHint::always(), aux); let vault = NoteAssets::new(assets)?; Ok(Note::new(vault, metadata, recipient)) @@ -83,14 +83,14 @@ pub fn create_p2ide_note( let serial_num = rng.draw_word(); let recipient = utils::build_p2ide_recipient(target, reclaim_height, timelock_height, serial_num)?; - let tag = NoteTag::from_account_id(target); + let tag = NoteTag::with_account_target(target); let execution_hint = match timelock_height { Some(height) => NoteExecutionHint::after_block(height)?, None => NoteExecutionHint::always(), }; - let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux)?; + let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux); let vault = NoteAssets::new(assets)?; Ok(Note::new(vault, metadata, recipient)) @@ -126,7 +126,7 @@ pub fn create_swap_note( let payback_recipient_word: Word = payback_recipient.digest(); let requested_asset_word: Word = requested_asset.into(); - let payback_tag = NoteTag::from_account_id(sender); + let payback_tag = NoteTag::with_account_target(sender); let inputs = NoteInputs::new(vec![ requested_asset_word[0], @@ -144,12 +144,12 @@ pub fn create_swap_note( ])?; // build the tag for the SWAP use case - let tag = build_swap_tag(swap_note_type, &offered_asset, &requested_asset)?; + let tag = build_swap_tag(swap_note_type, &offered_asset, &requested_asset); let serial_num = rng.draw_word(); // build the outgoing note let metadata = - NoteMetadata::new(sender, swap_note_type, tag, NoteExecutionHint::always(), swap_note_aux)?; + NoteMetadata::new(sender, swap_note_type, tag, NoteExecutionHint::always(), swap_note_aux); let assets = NoteAssets::new(vec![offered_asset])?; let recipient = NoteRecipient::new(serial_num, note_script, inputs); let note = Note::new(assets, metadata, recipient); @@ -200,9 +200,9 @@ pub fn create_mint_note( // Convert MintNoteInputs to NoteInputs let inputs = NoteInputs::from(mint_inputs); - let tag = NoteTag::from_account_id(faucet_id); + let tag = NoteTag::with_account_target(faucet_id); - let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux)?; + let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux); let assets = NoteAssets::new(vec![])?; // MINT notes have no assets let recipient = NoteRecipient::new(serial_num, note_script, inputs); @@ -246,9 +246,9 @@ pub fn create_burn_note( let execution_hint = NoteExecutionHint::always(); let inputs = NoteInputs::new(vec![])?; - let tag = NoteTag::from_account_id(faucet_id); + let tag = NoteTag::with_account_target(faucet_id); - let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux)?; + let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux); let assets = NoteAssets::new(vec![fungible_asset])?; // BURN notes contain the asset to burn let recipient = NoteRecipient::new(serial_num, note_script, inputs); diff --git a/crates/miden-standards/src/note/utils.rs b/crates/miden-standards/src/note/utils.rs index 6e3e408983..1d9fb780ee 100644 --- a/crates/miden-standards/src/note/utils.rs +++ b/crates/miden-standards/src/note/utils.rs @@ -1,7 +1,7 @@ use miden_protocol::account::AccountId; use miden_protocol::asset::Asset; use miden_protocol::block::BlockNumber; -use miden_protocol::note::{NoteExecutionMode, NoteInputs, NoteRecipient, NoteTag, NoteType}; +use miden_protocol::note::{NoteInputs, NoteRecipient, NoteTag, NoteType}; use miden_protocol::{Felt, NoteError, Word}; use super::well_known_note::WellKnownNote; @@ -47,18 +47,26 @@ pub fn build_p2ide_recipient( /// Returns a note tag for a swap note with the specified parameters. /// -/// Use case ID for the returned tag is set to 0. +/// The tag is laid out as follows: /// -/// Tag payload is constructed by taking asset tags (8 bits of each faucet ID) and concatenating -/// them together as offered_asset_tag + requested_asset tag. +/// ```text +/// [ +/// note_type (2 bits) | script_root (14 bits) +/// | offered_asset_faucet_id (8 bits) | requested_asset_faucet_id (8 bits) +/// ] +/// ``` /// -/// Network execution hint for the returned tag is set to `Local`. +/// The script root serves as the use case identifier of the SWAP tag. pub fn build_swap_tag( note_type: NoteType, offered_asset: &Asset, requested_asset: &Asset, -) -> Result { - const SWAP_USE_CASE_ID: u16 = 0; +) -> NoteTag { + let swap_root_bytes = WellKnownNote::SWAP.script().root().as_bytes(); + // Construct the swap use case ID from the 14 most significant bits of the script root. This + // leaves the two most significant bits zero. + let mut swap_use_case_id = (swap_root_bytes[0] as u16) << 6; + swap_use_case_id |= (swap_root_bytes[1] >> 2) as u16; // Get bits 0..8 from the faucet IDs of both assets which will form the tag payload. let offered_asset_id: u64 = offered_asset.faucet_id_prefix().into(); @@ -67,13 +75,12 @@ pub fn build_swap_tag( let requested_asset_id: u64 = requested_asset.faucet_id_prefix().into(); let requested_asset_tag = (requested_asset_id >> 56) as u8; - let payload = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16); + let asset_pair = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16); - let execution = NoteExecutionMode::Local; - match note_type { - NoteType::Public => NoteTag::for_public_use_case(SWAP_USE_CASE_ID, payload, execution), - _ => NoteTag::for_local_use_case(SWAP_USE_CASE_ID, payload), - } + let tag = + ((note_type as u8 as u32) << 30) | ((swap_use_case_id as u32) << 16) | asset_pair as u32; + + NoteTag::new(tag) } #[cfg(test)] @@ -129,16 +136,24 @@ mod tests { // The fungible ID starts with 0xcdb1. // The non fungible ID starts with 0xabec. // The expected tag payload is thus 0xcdab. - let expected_tag_payload = 0xcdab; - - let actual_tag = - build_swap_tag(NoteType::Public, &offered_asset, &requested_asset).unwrap(); - - // 0 is the SWAP use case ID. - let expected_tag = - NoteTag::for_public_use_case(0, expected_tag_payload, NoteExecutionMode::Local) - .unwrap(); - - assert_eq!(actual_tag, expected_tag); + let expected_asset_pair = 0xcdab; + + let note_type = NoteType::Public; + let actual_tag = build_swap_tag(note_type, &offered_asset, &requested_asset); + + assert_eq!(actual_tag.as_u32() as u16, expected_asset_pair, "asset pair should match"); + assert_eq!((actual_tag.as_u32() >> 30) as u8, note_type as u8, "note type should match"); + // Check the 8 bits of the first script root byte. + assert_eq!( + (actual_tag.as_u32() >> 22) as u8, + WellKnownNote::SWAP.script().root().as_bytes()[0], + "swap script root byte 0 should match" + ); + // Extract the 6 bits of the second script root byte and shift for comparison. + assert_eq!( + ((actual_tag.as_u32() & 0b00000000_00111111_00000000_00000000) >> 16) as u8, + WellKnownNote::SWAP.script().root().as_bytes()[1] >> 2, + "swap script root byte 1 should match with the lower two bits set to zero" + ); } } diff --git a/crates/miden-standards/src/testing/note.rs b/crates/miden-standards/src/testing/note.rs index 16451b9687..5d6a508900 100644 --- a/crates/miden-standards/src/testing/note.rs +++ b/crates/miden-standards/src/testing/note.rs @@ -57,7 +57,7 @@ impl NoteBuilder { note_execution_hint: NoteExecutionHint::None, serial_num, // The note tag is not under test, so we choose a value that is always valid. - tag: NoteTag::from_account_id(sender), + tag: NoteTag::with_account_target(sender), code: DEFAULT_NOTE_CODE.to_string(), aux: ZERO, dyn_libraries: Vec::new(), @@ -160,7 +160,7 @@ impl NoteBuilder { self.tag, self.note_execution_hint, self.aux, - )?; + ); let inputs = NoteInputs::new(self.inputs)?; let recipient = NoteRecipient::new(self.serial_num, note_script, inputs); diff --git a/crates/miden-testing/src/kernel_tests/tx/mod.rs b/crates/miden-testing/src/kernel_tests/tx/mod.rs index e59b033d0a..c3a9f84f77 100644 --- a/crates/miden-testing/src/kernel_tests/tx/mod.rs +++ b/crates/miden-testing/src/kernel_tests/tx/mod.rs @@ -1,5 +1,3 @@ -use alloc::string::String; - use anyhow::Context; use miden_processor::ContextId; use miden_processor::fast::ExecutionOutput; @@ -11,19 +9,7 @@ use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, ACCOUNT_ID_SENDER, }; -use miden_protocol::testing::storage::prepare_assets; -use miden_protocol::transaction::memory::{ - self, - MemoryOffset, - NOTE_MEM_SIZE, - NUM_OUTPUT_NOTES_PTR, - OUTPUT_NOTE_ASSETS_OFFSET, - OUTPUT_NOTE_DIRTY_FLAG_OFFSET, - OUTPUT_NOTE_METADATA_OFFSET, - OUTPUT_NOTE_NUM_ASSETS_OFFSET, - OUTPUT_NOTE_RECIPIENT_OFFSET, - OUTPUT_NOTE_SECTION_OFFSET, -}; +use miden_protocol::transaction::memory::{self, MemoryOffset}; use miden_protocol::vm::StackInputs; use miden_protocol::{Felt, Word, ZERO}; @@ -117,66 +103,6 @@ pub fn input_note_data_ptr(note_idx: u32) -> memory::MemoryAddress { memory::INPUT_NOTE_DATA_SECTION_OFFSET + note_idx * memory::NOTE_MEM_SIZE } -/// Returns MASM code that defines a procedure called `create_mock_notes` which creates the notes -/// specified in `notes`, which stores output note metadata in the transaction host's memory. -pub fn create_mock_notes_procedure(notes: &[Note]) -> String { - if notes.is_empty() { - return String::new(); - } - - let mut script = String::from( - "proc create_mock_notes - # remove padding from prologue - dropw dropw dropw dropw - ", - ); - - for (idx, note) in notes.iter().enumerate() { - let metadata = Word::from(note.metadata()); - let recipient = note.recipient().digest(); - let assets = prepare_assets(note.assets()); - let num_assets = assets.len(); - let note_offset = (idx as u32) * NOTE_MEM_SIZE; - - assert!(num_assets == 1, "notes are expected to have one asset only"); - - script.push_str(&format!( - " - # populate note {idx} - push.{metadata} - push.{OUTPUT_NOTE_SECTION_OFFSET} push.{note_offset} push.{OUTPUT_NOTE_METADATA_OFFSET} add add mem_storew_be dropw - - push.{recipient} - push.{OUTPUT_NOTE_SECTION_OFFSET} push.{note_offset} push.{OUTPUT_NOTE_RECIPIENT_OFFSET} add add mem_storew_be dropw - - push.{num_assets} - push.{OUTPUT_NOTE_SECTION_OFFSET} push.{note_offset} push.{OUTPUT_NOTE_NUM_ASSETS_OFFSET} add add mem_store - - push.1 # dirty flag should be `1` by default - push.{OUTPUT_NOTE_SECTION_OFFSET} push.{note_offset} push.{OUTPUT_NOTE_DIRTY_FLAG_OFFSET} add add mem_store - - push.{first_asset} - push.{OUTPUT_NOTE_SECTION_OFFSET} push.{note_offset} push.{OUTPUT_NOTE_ASSETS_OFFSET} add add mem_storew_be dropw - ", - idx = idx, - metadata = metadata, - recipient = recipient, - num_assets = num_assets, - first_asset = assets[0], - note_offset = note_offset, - )); - } - script.push_str(&format!( - "# set num output notes - push.{count} push.{NUM_OUTPUT_NOTES_PTR} mem_store - end - ", - count = notes.len(), - )); - - script -} - // HELPER STRUCTURE // ================================================================================================ diff --git a/crates/miden-testing/src/kernel_tests/tx/test_account.rs b/crates/miden-testing/src/kernel_tests/tx/test_account.rs index 47cdc60564..d0146be16b 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account.rs @@ -1267,7 +1267,7 @@ async fn test_get_init_balance_subtraction() -> anyhow::Result<()> { begin # create random note and move the asset into it - exec.util::create_random_note + exec.util::create_default_note # => [note_idx] push.{REMOVED_ASSET} diff --git a/crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs b/crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs index bf2473d7f5..da54be8b2c 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs @@ -599,22 +599,15 @@ async fn asset_and_storage_delta() -> anyhow::Result<()> { let removed_assets = [removed_asset_1, removed_asset_2, removed_asset_3]; let tag1 = - NoteTag::from_account_id(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?); - let tag2 = NoteTag::for_local_use_case(0, 0)?; - let tag3 = NoteTag::for_local_use_case(0, 0)?; + NoteTag::with_account_target(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into()?); + let tag2 = NoteTag::default(); + let tag3 = NoteTag::default(); let tags = [tag1, tag2, tag3]; let aux_array = [Felt::new(27), Felt::new(28), Felt::new(29)]; let note_types = [NoteType::Private; 3]; - tag1.validate(NoteType::Private) - .expect("note tag 1 should support private notes"); - tag2.validate(NoteType::Private) - .expect("note tag 2 should support private notes"); - tag3.validate(NoteType::Private) - .expect("note tag 3 should support private notes"); - let execution_hint_1 = Felt::from(NoteExecutionHint::always()); let execution_hint_2 = Felt::from(NoteExecutionHint::none()); let execution_hint_3 = Felt::from(NoteExecutionHint::on_block_slot(1, 1, 1)); diff --git a/crates/miden-testing/src/kernel_tests/tx/test_account_interface.rs b/crates/miden-testing/src/kernel_tests/tx/test_account_interface.rs index 8be831ff05..ef250d634c 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account_interface.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account_interface.rs @@ -793,10 +793,9 @@ fn create_p2ide_note_with_inputs(inputs: impl IntoIterator, sender: NoteInputs::new(inputs.into_iter().map(Felt::new).collect()).unwrap(), ); - let tag = NoteTag::from_account_id(sender); + let tag = NoteTag::with_account_target(sender); let metadata = - NoteMetadata::new(sender, NoteType::Public, tag, NoteExecutionHint::always(), ZERO) - .unwrap(); + NoteMetadata::new(sender, NoteType::Public, tag, NoteExecutionHint::always(), ZERO); Note::new(NoteAssets::default(), metadata, recipient) } diff --git a/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs index f24642ae3e..b23404dd27 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_active_note.rs @@ -402,15 +402,14 @@ async fn test_active_note_get_exactly_8_inputs() -> anyhow::Result<()> { // prepare note data let serial_num = RpoRandomCoin::new(Word::from([4u32; 4])).draw_word(); - let tag = NoteTag::from_account_id(target_id); + let tag = NoteTag::with_account_target(target_id); let metadata = NoteMetadata::new( sender_id, NoteType::Public, tag, NoteExecutionHint::always(), Default::default(), - ) - .context("failed to create metadata")?; + ); let vault = NoteAssets::new(vec![]).context("failed to create input note assets")?; let note_script = CodeBuilder::default() .compile_note_script("begin nop end") diff --git a/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs b/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs index 16f1c564ff..79c7b608dc 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs @@ -1,6 +1,8 @@ use alloc::string::ToString; use alloc::vec::Vec; +use std::borrow::ToOwned; +use miden_processor::crypto::RpoRandomCoin; use miden_processor::{Felt, ONE}; use miden_protocol::Word; use miden_protocol::account::{Account, AccountDelta, AccountStorageDelta, AccountVaultDelta}; @@ -17,7 +19,6 @@ use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, - ACCOUNT_ID_SENDER, }; use miden_protocol::testing::storage::MOCK_VALUE_SLOT0; use miden_protocol::transaction::memory::{ @@ -30,9 +31,9 @@ use miden_standards::code_builder::CodeBuilder; use miden_standards::testing::mock_account::MockAccountExt; use miden_standards::testing::note::NoteBuilder; -use super::{ZERO, create_mock_notes_procedure}; +use super::ZERO; use crate::kernel_tests::tx::ExecutionOutputExt; -use crate::utils::{create_public_p2any_note, create_spawn_note}; +use crate::utils::{create_p2any_note, create_public_p2any_note}; use crate::{ Auth, MockChain, @@ -42,49 +43,56 @@ use crate::{ assert_transaction_executor_error, }; +/// Tests that the return values from the tx kernel main.masm program match the expected values. #[tokio::test] async fn test_epilogue() -> anyhow::Result<()> { let account = Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); - let tx_context = { - let output_note_1 = create_public_p2any_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - [FungibleAsset::mock(100)], - ); - - // input_note_1 is needed for maintaining cohesion of involved assets - let input_note_1 = create_public_p2any_note( - ACCOUNT_ID_SENDER.try_into().unwrap(), - [FungibleAsset::mock(100)], - ); - let input_note_2 = create_spawn_note([&output_note_1])?; - TransactionContextBuilder::new(account.clone()) - .extend_input_notes(vec![input_note_1, input_note_2]) - .extend_expected_output_notes(vec![OutputNote::Full(output_note_1)]) - .build()? - }; - - let output_notes_data_procedure = - create_mock_notes_procedure(tx_context.expected_output_notes()); + let asset = FungibleAsset::mock(100); + let output_note_1 = create_public_p2any_note(account.id(), [asset]); + // input_note_1 is needed for maintaining cohesion of involved assets + let input_note_1 = + create_public_p2any_note(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1.try_into().unwrap(), [asset]); + + let tx_context = TransactionContextBuilder::new(account.clone()) + .extend_input_notes(vec![input_note_1]) + .extend_expected_output_notes(vec![OutputNote::Full(output_note_1.clone())]) + .build()?; let code = format!( " use $kernel::prologue use $kernel::account use $kernel::epilogue - - {output_notes_data_procedure} + use miden::protocol::output_note + use miden::core::sys begin exec.prologue::prepare_transaction - exec.create_mock_notes + push.{recipient} + push.{note_execution_hint} + push.{note_type} + push.{aux} + push.{tag} + exec.output_note::create + # => [note_idx] + + push.{asset} + exec.output_note::add_asset + # => [] exec.epilogue::finalize_transaction # truncate the stack - repeat.13 movup.13 drop end + exec.sys::truncate_stack end - " + ", + recipient = output_note_1.recipient().digest(), + note_execution_hint = Felt::from(output_note_1.metadata().execution_hint()), + note_type = Felt::from(output_note_1.metadata().note_type()), + aux = output_note_1.metadata().aux(), + tag = Felt::from(output_note_1.metadata().tag()), + asset = Word::from(asset) ); let exec_output = tx_context.execute_code(&code).await?; @@ -144,48 +152,74 @@ async fn test_epilogue() -> anyhow::Result<()> { Ok(()) } +/// Tests that the output note memory section is correctly populated during finalize_transaction. #[tokio::test] async fn test_compute_output_note_id() -> anyhow::Result<()> { - let tx_context = { - let account = - Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); - let output_note_1 = - create_public_p2any_note(ACCOUNT_ID_SENDER.try_into()?, [FungibleAsset::mock(100)]); - - // input_note_1 is needed for maintaining cohesion of involved assets - let input_note_1 = - create_public_p2any_note(ACCOUNT_ID_SENDER.try_into()?, [FungibleAsset::mock(100)]); - let input_note_2 = create_spawn_note([&output_note_1])?; - TransactionContextBuilder::new(account) - .extend_input_notes(vec![input_note_1, input_note_2]) - .extend_expected_output_notes(vec![OutputNote::Full(output_note_1)]) - .build()? - }; - - let output_notes_data_procedure = - create_mock_notes_procedure(tx_context.expected_output_notes()); - - for (note, i) in tx_context.expected_output_notes().iter().zip(0u32..) { - let code = format!( - " + let mut rng = RpoRandomCoin::new(Word::from([3, 4, 5, 6u32])); + let account = Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, Auth::IncrNonce); + let mut assets = account.vault().assets(); + let asset0 = assets.next().unwrap(); + let asset1 = assets.next().unwrap(); + + let output_note0 = create_p2any_note(account.id(), NoteType::Private, [asset0], &mut rng); + let output_note1 = create_p2any_note(account.id(), NoteType::Private, [asset1], &mut rng); + + let tx_context = TransactionContextBuilder::new(account.clone()) + .extend_expected_output_notes(vec![ + OutputNote::Full(output_note0.clone()), + OutputNote::Full(output_note1.clone()), + ]) + .build()?; + + let mut code = " use $kernel::prologue use $kernel::epilogue - - {output_notes_data_procedure} + use miden::protocol::output_note + use miden::core::sys begin - exec.prologue::prepare_transaction - exec.create_mock_notes - exec.epilogue::finalize_transaction + exec.prologue::prepare_transaction" + .to_owned(); + + for note in tx_context.expected_output_notes() { + let asset = note.assets().iter().next().unwrap(); - # truncate the stack - repeat.13 movup.13 drop end - end + code.push_str(&format!( " - ); + push.{recipient} + push.{note_execution_hint} + push.{note_type} + push.{aux} + push.{tag} + exec.output_note::create + # => [note_idx] + + push.{asset} + call.::miden::standards::wallets::basic::move_asset_to_note + # => [] + ", + recipient = note.recipient().digest(), + note_execution_hint = Felt::from(note.metadata().execution_hint()), + note_type = Felt::from(note.metadata().note_type()), + aux = note.metadata().aux(), + tag = Felt::from(note.metadata().tag()), + asset = Word::from(asset) + )); + } - let exec_output = &tx_context.execute_code(&code).await?; + code.push_str( + " + exec.epilogue::finalize_transaction + + # truncate the stack + exec.sys::truncate_stack + end", + ); + + let exec_output = &tx_context.execute_code(&code).await?; + for (i, note) in tx_context.expected_output_notes().iter().enumerate() { + let i = i as u32; assert_eq!( note.assets().commitment(), exec_output.get_kernel_mem_word( @@ -202,6 +236,7 @@ async fn test_compute_output_note_id() -> anyhow::Result<()> { "NOTE_ID didn't match expected value", ); } + Ok(()) } @@ -232,7 +267,7 @@ async fn epilogue_fails_when_num_output_assets_exceed_num_input_assets() -> anyh begin # create a note with the output asset push.{OUTPUT_ASSET} - exec.util::create_random_note_with_asset + exec.util::create_default_note_with_asset # => [] end ", @@ -285,7 +320,7 @@ async fn epilogue_fails_when_num_input_assets_exceed_num_output_assets() -> anyh begin # create a note with the output asset push.{OUTPUT_ASSET} - exec.util::create_random_note_with_asset + exec.util::create_default_note_with_asset # => [] end ", @@ -530,7 +565,7 @@ async fn test_epilogue_execute_empty_transaction() -> anyhow::Result<()> { #[tokio::test] async fn test_epilogue_empty_transaction_with_empty_output_note() -> anyhow::Result<()> { let tag = - NoteTag::from_account_id(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE.try_into()?); + NoteTag::with_account_target(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE.try_into()?); let aux = Felt::new(26); let note_type = NoteType::Private; diff --git a/crates/miden-testing/src/kernel_tests/tx/test_lazy_loading.rs b/crates/miden-testing/src/kernel_tests/tx/test_lazy_loading.rs index 4372650953..cf0216fa6d 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_lazy_loading.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_lazy_loading.rs @@ -96,7 +96,7 @@ async fn removing_fungible_assets_with_lazy_loading_succeeds() -> anyhow::Result # => [] # move asset to note to adhere to asset preservation rules - exec.util::create_random_note_with_asset + exec.util::create_default_note_with_asset # => [] push.{FUNGIBLE_ASSET2} @@ -104,7 +104,7 @@ async fn removing_fungible_assets_with_lazy_loading_succeeds() -> anyhow::Result # => [ASSET] # move asset to note to adhere to asset preservation rules - exec.util::create_random_note_with_asset + exec.util::create_default_note_with_asset # => [] 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 c8dec803f5..f4ceaea6ac 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_note.rs @@ -15,7 +15,6 @@ use miden_protocol::note::{ Note, NoteAssets, NoteExecutionHint, - NoteExecutionMode, NoteInputs, NoteMetadata, NoteRecipient, @@ -392,22 +391,18 @@ async fn test_build_metadata() -> miette::Result<()> { let test_metadata1 = NoteMetadata::new( sender, NoteType::Private, - NoteTag::from_account_id(receiver), + NoteTag::with_account_target(receiver), NoteExecutionHint::after_block(500.into()) .map_err(|e| miette::miette!("Failed to create execution hint: {}", e))?, Felt::try_from(1u64 << 63).map_err(|e| miette::miette!("Failed to convert felt: {}", e))?, - ) - .map_err(|e| miette::miette!("Failed to create metadata: {}", e))?; + ); let test_metadata2 = NoteMetadata::new( sender, NoteType::Public, - // Use largest allowed use_case_id. - NoteTag::for_public_use_case((1 << 14) - 1, u16::MAX, NoteExecutionMode::Local) - .map_err(|e| miette::miette!("Failed to create note tag: {}", e))?, + NoteTag::new(u32::MAX), NoteExecutionHint::on_block_slot(u8::MAX, u8::MAX, u8::MAX), Felt::try_from(0u64).map_err(|e| miette::miette!("Failed to convert felt: {}", e))?, - ) - .map_err(|e| miette::miette!("Failed to create metadata: {}", e))?; + ); for (iteration, test_metadata) in [test_metadata1, test_metadata2].into_iter().enumerate() { let code = format!( @@ -545,14 +540,14 @@ async fn test_public_key_as_note_input() -> anyhow::Result<()> { .build_existing()?; let serial_num = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); - let tag = NoteTag::from_account_id(target_account.id()); + let tag = NoteTag::with_account_target(target_account.id()); let metadata = NoteMetadata::new( sender_account.id(), NoteType::Public, tag, NoteExecutionHint::always(), Default::default(), - )?; + ); let vault = NoteAssets::new(vec![])?; let note_script = CodeBuilder::default().compile_note_script("begin nop end")?; let recipient = @@ -573,7 +568,7 @@ async fn test_build_note_tag_for_network_account() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let account_id = AccountId::try_from(ACCOUNT_ID_NETWORK_FUNGIBLE_FAUCET)?; - let expected_tag = NoteTag::from_account_id(account_id).as_u32(); + let expected_tag = NoteTag::with_account_target(account_id).as_u32(); let prefix: u64 = account_id.prefix().into(); let suffix: u64 = account_id.suffix().into(); diff --git a/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs index 9797291131..a998663174 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_output_note.rs @@ -13,7 +13,6 @@ use miden_protocol::note::{ Note, NoteAssets, NoteExecutionHint, - NoteExecutionMode, NoteInputs, NoteMetadata, NoteRecipient, @@ -58,7 +57,7 @@ async fn test_create_note() -> anyhow::Result<()> { let recipient = Word::from([0, 1, 2, 3u32]); let aux = Felt::new(27); - let tag = NoteTag::from_account_id(account_id); + let tag = NoteTag::with_account_target(account_id); let code = format!( " @@ -107,7 +106,7 @@ async fn test_create_note() -> anyhow::Result<()> { tag, NoteExecutionHint::after_block(23.into())?, Felt::new(27), - )? + ) .into(); assert_eq!( @@ -129,7 +128,7 @@ async fn test_create_note_with_invalid_tag() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let invalid_tag = Felt::new((NoteType::Public as u64) << 62); - let valid_tag: Felt = NoteTag::for_local_use_case(0, 0).unwrap().into(); + let valid_tag: Felt = NoteTag::default().into(); // Test invalid tag assert!(tx_context.execute_code(¬e_creation_script(invalid_tag)).await.is_err()); @@ -193,7 +192,7 @@ async fn test_create_note_too_many_notes() -> anyhow::Result<()> { call.output_note::create end ", - tag = NoteTag::for_local_use_case(1234, 5678).unwrap(), + tag = NoteTag::new(1234 << 16 | 5678), recipient = Word::from([0, 1, 2, 3u32]), execution_hint_always = Felt::from(NoteExecutionHint::always()), PUBLIC_NOTE = NoteType::Public as u8, @@ -255,7 +254,7 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { // create output note 1 let output_serial_no_1 = Word::from([8u32; 4]); - let output_tag_1 = NoteTag::from_account_id(network_account); + let output_tag_1 = NoteTag::with_account_target(network_account); let assets = NoteAssets::new(vec![input_asset_1])?; let metadata = NoteMetadata::new( tx_context.tx_inputs().account().id(), @@ -263,14 +262,14 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { output_tag_1, NoteExecutionHint::Always, ZERO, - )?; + ); let inputs = NoteInputs::new(vec![])?; let recipient = NoteRecipient::new(output_serial_no_1, input_note_1.script().clone(), inputs); let output_note_1 = Note::new(assets, metadata, recipient); // create output note 2 let output_serial_no_2 = Word::from([11u32; 4]); - let output_tag_2 = NoteTag::from_account_id(local_account); + let output_tag_2 = NoteTag::with_account_target(local_account); let assets = NoteAssets::new(vec![input_asset_2])?; let metadata = NoteMetadata::new( tx_context.tx_inputs().account().id(), @@ -278,7 +277,7 @@ async fn test_get_output_notes_commitment() -> anyhow::Result<()> { output_tag_2, NoteExecutionHint::after_block(123.into())?, ZERO, - )?; + ); let inputs = NoteInputs::new(vec![])?; let recipient = NoteRecipient::new(output_serial_no_2, input_note_2.script().clone(), inputs); let output_note_2 = Note::new(assets, metadata, recipient); @@ -392,7 +391,7 @@ async fn test_create_note_and_add_asset() -> anyhow::Result<()> { let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?; let recipient = Word::from([0, 1, 2, 3u32]); let aux = Felt::new(27); - let tag = NoteTag::from_account_id(faucet_id); + let tag = NoteTag::with_account_target(faucet_id); let asset = Word::from(FungibleAsset::new(faucet_id, 10)?); let code = format!( @@ -454,7 +453,7 @@ async fn test_create_note_and_add_multiple_assets() -> anyhow::Result<()> { let recipient = Word::from([0, 1, 2, 3u32]); let aux = Felt::new(27); - let tag = NoteTag::from_account_id(faucet_2); + let tag = NoteTag::with_account_target(faucet_2); let asset = Word::from(FungibleAsset::new(faucet, 10)?); let asset_2 = Word::from(FungibleAsset::new(faucet_2, 20)?); @@ -541,7 +540,7 @@ async fn test_create_note_and_add_same_nft_twice() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let recipient = Word::from([0, 1, 2, 3u32]); - let tag = NoteTag::for_public_use_case(999, 777, NoteExecutionMode::Local).unwrap(); + let tag = NoteTag::new(999 << 16 | 777); let non_fungible_asset = NonFungibleAsset::mock(&[1, 2, 3]); let encoded = Word::from(non_fungible_asset); @@ -631,7 +630,7 @@ async fn test_build_recipient_hash() -> anyhow::Result<()> { // create output note let output_serial_no = Word::from([0, 1, 2, 3u32]); let aux = Felt::new(27); - let tag = NoteTag::for_public_use_case(42, 42, NoteExecutionMode::Network).unwrap(); + let tag = NoteTag::new(42 << 16 | 42); let single_input = 2; let inputs = NoteInputs::new(vec![Felt::new(single_input)]).unwrap(); let input_commitment = inputs.commitment(); diff --git a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs index 1714c4b9bb..d9641af5e5 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs @@ -22,7 +22,6 @@ use miden_protocol::note::{ Note, NoteAssets, NoteExecutionHint, - NoteExecutionMode, NoteHeader, NoteId, NoteInputs, @@ -198,11 +197,11 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { .expect("asset is valid"), ); - let tag1 = NoteTag::from_account_id( + let tag1 = NoteTag::with_account_target( ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap(), ); - let tag2 = NoteTag::for_public_use_case(0, 0, NoteExecutionMode::Local).unwrap(); - let tag3 = NoteTag::for_public_use_case(0, 0, NoteExecutionMode::Local).unwrap(); + let tag2 = NoteTag::default(); + let tag3 = NoteTag::default(); let aux1 = Felt::new(27); let aux2 = Felt::new(28); let aux3 = Felt::new(29); @@ -211,10 +210,6 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { let note_type2 = NoteType::Public; let note_type3 = NoteType::Public; - tag1.validate(note_type1).expect("note tag 1 should support private notes"); - tag2.validate(note_type2).expect("note tag 2 should support public notes"); - tag3.validate(note_type3).expect("note tag 3 should support public notes"); - // In this test we create 3 notes. Note 1 is private, Note 2 is public and Note 3 is public // without assets. @@ -223,7 +218,7 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { let note_script_2 = CodeBuilder::default().compile_note_script(DEFAULT_NOTE_CODE)?; let inputs_2 = NoteInputs::new(vec![ONE])?; let metadata_2 = - NoteMetadata::new(account_id, note_type2, tag2, NoteExecutionHint::none(), aux2)?; + NoteMetadata::new(account_id, note_type2, tag2, NoteExecutionHint::none(), aux2); let vault_2 = NoteAssets::new(vec![removed_asset_3, removed_asset_4])?; let recipient_2 = NoteRecipient::new(serial_num_2, note_script_2, inputs_2); let expected_output_note_2 = Note::new(vault_2, metadata_2, recipient_2); @@ -238,7 +233,7 @@ async fn executed_transaction_output_notes() -> anyhow::Result<()> { tag3, NoteExecutionHint::on_block_slot(1, 2, 3), aux3, - )?; + ); let vault_3 = NoteAssets::new(vec![])?; let recipient_3 = NoteRecipient::new(serial_num_3, note_script_3, inputs_3); let expected_output_note_3 = Note::new(vault_3, metadata_3, recipient_3); diff --git a/crates/miden-testing/tests/lib.rs b/crates/miden-testing/tests/lib.rs index 37339b3b25..0c3c1d790f 100644 --- a/crates/miden-testing/tests/lib.rs +++ b/crates/miden-testing/tests/lib.rs @@ -64,8 +64,7 @@ pub fn get_note_with_fungible_asset_and_script( let vault = NoteAssets::new(vec![fungible_asset.into()]).unwrap(); let metadata = - NoteMetadata::new(sender_id, NoteType::Public, 1.into(), NoteExecutionHint::Always, ZERO) - .unwrap(); + NoteMetadata::new(sender_id, NoteType::Public, 1.into(), NoteExecutionHint::Always, ZERO); let inputs = NoteInputs::new(vec![]).unwrap(); let recipient = NoteRecipient::new(serial_num, note_script, inputs); diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index 38b378a903..d6edecea48 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -17,7 +17,6 @@ use miden_protocol::note::{ Note, NoteAssets, NoteExecutionHint, - NoteExecutionMode, NoteId, NoteInputs, NoteMetadata, @@ -127,7 +126,7 @@ pub fn verify_minted_output_note( params.tag, params.note_execution_hint, params.aux - )? + ) ); Ok(()) @@ -145,18 +144,13 @@ async fn minting_fungible_asset_on_existing_faucet_succeeds() -> anyhow::Result< let params = FaucetTestParams { recipient: Word::from([0, 1, 2, 3u32]), - tag: NoteTag::for_local_use_case(0, 0).unwrap(), + tag: NoteTag::default(), aux: Felt::new(27), note_execution_hint: NoteExecutionHint::on_block_slot(5, 6, 7), note_type: NoteType::Private, amount: Felt::new(100), }; - params - .tag - .validate(params.note_type) - .expect("note tag should support private notes"); - let executed_transaction = execute_mint_transaction(&mut mock_chain, faucet.clone(), ¶ms).await?; verify_minted_output_note(&executed_transaction, &faucet, ¶ms)?; @@ -230,18 +224,13 @@ async fn minting_fungible_asset_on_new_faucet_succeeds() -> anyhow::Result<()> { let params = FaucetTestParams { recipient: Word::from([0, 1, 2, 3u32]), - tag: NoteTag::for_local_use_case(0, 0).unwrap(), + tag: NoteTag::default(), aux: Felt::new(27), note_execution_hint: NoteExecutionHint::on_block_slot(5, 6, 7), note_type: NoteType::Private, amount: Felt::new(100), }; - params - .tag - .validate(params.note_type) - .expect("note tag should support private notes"); - let executed_transaction = execute_mint_transaction(&mut mock_chain, faucet.clone(), ¶ms).await?; verify_minted_output_note(&executed_transaction, &faucet, ¶ms)?; @@ -324,7 +313,7 @@ async fn test_public_note_creation_with_script_from_datastore() -> anyhow::Resul // Parameters for the PUBLIC note that will be created by the faucet let recipient_account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?; let amount = Felt::new(75); - let tag = NoteTag::for_public_use_case(0, 0, NoteExecutionMode::Local)?; + let tag = NoteTag::default(); let aux = Felt::new(27); let note_execution_hint = NoteExecutionHint::on_block_slot(5, 6, 7); let note_type = NoteType::Public; @@ -357,7 +346,7 @@ async fn test_public_note_creation_with_script_from_datastore() -> anyhow::Resul let output_script_root = note_recipient.script().root(); let asset = FungibleAsset::new(faucet.id(), amount.into())?; - let metadata = NoteMetadata::new(faucet.id(), note_type, tag, note_execution_hint, aux)?; + let metadata = NoteMetadata::new(faucet.id(), note_type, tag, note_execution_hint, aux); let expected_note = Note::new(NoteAssets::new(vec![asset.into()])?, metadata, note_recipient); let trigger_note_script_code = format!( @@ -422,7 +411,7 @@ async fn test_public_note_creation_with_script_from_datastore() -> anyhow::Resul let mut rng = RpoRandomCoin::new([Felt::from(1u32); 4].into()); let trigger_note = NoteBuilder::new(faucet.id(), &mut rng) .note_type(NoteType::Private) - .tag(NoteTag::for_local_use_case(0, 0)?.into()) + .tag(NoteTag::default().into()) .note_execution_hint(NoteExecutionHint::always()) .aux(Felt::new(0)) .serial_number(Word::from([1, 2, 3, 4u32])) @@ -532,7 +521,7 @@ async fn network_faucet_mint() -> anyhow::Result<()> { let aux = Felt::new(27); let serial_num = Word::default(); - let output_note_tag = NoteTag::from_account_id(target_account.id()); + let output_note_tag = NoteTag::with_account_target(target_account.id()); let p2id_mint_output_note = create_p2id_note_exact( faucet.id(), target_account.id(), @@ -715,7 +704,7 @@ async fn test_mint_note_output_note_types(#[case] note_type: NoteType) -> anyhow // Create MINT note based on note type let mint_inputs = match note_type { NoteType::Private => { - let output_note_tag = NoteTag::from_account_id(target_account.id()); + let output_note_tag = NoteTag::with_account_target(target_account.id()); let recipient = p2id_mint_output_note.recipient().digest(); MintNoteInputs::new_private( recipient, @@ -726,7 +715,7 @@ async fn test_mint_note_output_note_types(#[case] note_type: NoteType) -> anyhow ) }, NoteType::Public => { - let output_note_tag = NoteTag::from_account_id(target_account.id()); + let output_note_tag = NoteTag::with_account_target(target_account.id()); let p2id_script = WellKnownNote::P2ID.script(); let p2id_inputs = vec![target_account.id().suffix(), target_account.id().prefix().as_felt()]; diff --git a/crates/miden-testing/tests/scripts/send_note.rs b/crates/miden-testing/tests/scripts/send_note.rs index 083993039d..84d8c02934 100644 --- a/crates/miden-testing/tests/scripts/send_note.rs +++ b/crates/miden-testing/tests/scripts/send_note.rs @@ -35,14 +35,14 @@ async fn test_send_note_script_basic_wallet() -> anyhow::Result<()> { let sender_account_interface = AccountInterface::from_account(&sender_basic_wallet_account); - let tag = NoteTag::from_account_id(sender_basic_wallet_account.id()); + let tag = NoteTag::with_account_target(sender_basic_wallet_account.id()); let metadata = NoteMetadata::new( sender_basic_wallet_account.id(), NoteType::Public, tag, NoteExecutionHint::always(), Default::default(), - )?; + ); let assets = NoteAssets::new(vec![sent_asset]).unwrap(); let note_script = CodeBuilder::default().compile_note_script("begin nop end").unwrap(); let serial_num = RpoRandomCoin::new(Word::from([1, 2, 3, 4u32])).draw_word(); @@ -95,14 +95,14 @@ async fn test_send_note_script_basic_fungible_faucet() -> anyhow::Result<()> { let sender_account_interface = AccountInterface::from_account(&sender_basic_fungible_faucet_account); - let tag = NoteTag::from_account_id(sender_basic_fungible_faucet_account.id()); + let tag = NoteTag::with_account_target(sender_basic_fungible_faucet_account.id()); let metadata = NoteMetadata::new( sender_basic_fungible_faucet_account.id(), NoteType::Public, tag, NoteExecutionHint::always(), Default::default(), - )?; + ); let assets = NoteAssets::new(vec![Asset::Fungible( FungibleAsset::new(sender_basic_fungible_faucet_account.id(), 10).unwrap(), )])?; diff --git a/crates/miden-testing/tests/scripts/swap.rs b/crates/miden-testing/tests/scripts/swap.rs index bce9193a6c..777adb0a69 100644 --- a/crates/miden-testing/tests/scripts/swap.rs +++ b/crates/miden-testing/tests/scripts/swap.rs @@ -358,9 +358,9 @@ pub fn create_p2id_note_exact( ) -> Result { let recipient = utils::build_p2id_recipient(target, serial_num)?; - let tag = NoteTag::from_account_id(target); + let tag = NoteTag::with_account_target(target); - let metadata = NoteMetadata::new(sender, note_type, tag, NoteExecutionHint::always(), aux)?; + let metadata = NoteMetadata::new(sender, note_type, tag, NoteExecutionHint::always(), aux); let vault = NoteAssets::new(assets)?; Ok(Note::new(vault, metadata, recipient)) diff --git a/docs/src/note.md b/docs/src/note.md index 20f30201f2..276007652a 100644 --- a/docs/src/note.md +++ b/docs/src/note.md @@ -98,7 +98,26 @@ After validation notes become "live" and eligible for consumption. If creation a Clients often need to find specific notes of interest. Miden allows clients to query the `Note` database using `Note` tags. These lightweight, 32-bit data fields serve as best-effort filters, enabling quick lookups for notes related to particular use cases, scripts, or account prefixes. -Using `Note` tags strikes a balance between privacy and efficiency. Without tags, querying a specific `Note` ID reveals a user’s interest to the operator. Conversely, downloading and filtering all registered notes locally is highly inefficient. Tags allow users to adjust their level of privacy by choosing how broadly or narrowly they define their search criteria, letting them find the right balance between revealing too much information and incurring excessive computational overhead. +While note tags can be arbitrarily constructed from 32 bits of data, there are two categories of tags that many notes fit into. + +#### Account Targets + +A note targeted at an account is a note that is intended or even enforced to be consumed by a specific account. One example is a P2ID note that can only be consumed by a specific account ID. The tag for such a note should make it easy for the receiver to find the note. Therefore, the tag encodes a certain number of bits of the receiver account's ID, by convention. Notably, it may not encode the full 32 bits of the target account's ID to preserve the receiver's privacy. See also the section on privacy below. + +#### Use Case Tags + +Use case notes are notes that are not intended to be consumed by a specific account, but by anyone willing to fulfill the note's contract. One example is a SWAP note that trades one asset against another. Such a use case note can define the structure of their note tags. A sensible structure for a SWAP note could be: +- encoding the 2 bits of the note's type. +- encoding the note script root, i.e. making it identifiable as a SWAP note, for example by + using 16 bits of the SWAP script root. +- encoding the SWAP pair, for example by using 8 bits of the offered asset faucet ID and 8 bits + of the requested asset faucet ID. + +This allows clients to search for a public SWAP note that trades USDC against ETH only through the note tag. Since tags are not validated in any way and only act as best-effort filters, further local filtering is almost always necessary. For example, there could easily be a collision on the 8 bits used in SWAP tag's faucet IDs. + +#### Privacy vs Efficiency + +Using `Note` tags strikes a balance between privacy and efficiency. Without tags, querying a specific `Note` ID reveals a user's interest to the operator. Conversely, downloading and filtering all registered notes locally is highly inefficient. Tags allow users to adjust their level of privacy by choosing how broadly or narrowly they define their search criteria, letting them find the right balance between revealing too much information and incurring excessive computational overhead. ### Note consumption