miden_protocol/note/note_tag.rs
1use core::fmt;
2
3use miden_crypto::Felt;
4
5use super::{
6 AccountId,
7 ByteReader,
8 ByteWriter,
9 Deserializable,
10 DeserializationError,
11 NoteError,
12 Serializable,
13};
14// NOTE TAG
15// ================================================================================================
16
17/// [`NoteTag`]s are 32-bits of data that serve as best-effort filters for notes.
18///
19/// Tags enable quick lookups for notes related to particular use cases, scripts, or account
20/// prefixes.
21///
22/// ## Account Targets
23///
24/// A note targeted at an account is a note that is intended or even enforced to be consumed by a
25/// specific account. One example is a P2ID note that can only be consumed by a specific account ID.
26/// The tag for such a note should make it easy for the receiver to find the note. Therefore, the
27/// tag encodes a certain number of bits of the receiver account's ID, by convention. Notably, it
28/// may not encode the full 32 bits of the target account's ID to preserve the receiver's privacy.
29/// See also the section on privacy below.
30///
31/// Because this convention is widely used, the note tag provides a dedicated constructor for this:
32/// [`NoteTag::with_account_target`].
33///
34/// ## Use Case Tags
35///
36/// Use case notes are notes that are not intended to be consumed by a specific account, but by
37/// anyone willing to fulfill the note's contract. One example is a SWAP note that trades one asset
38/// against another. Such a use case note can define the structure of their note tags. A sensible
39/// structure for a SWAP note could be:
40/// - encoding the 2 bits of the note's type.
41/// - encoding the note script root, i.e. making it identifiable as a SWAP note, for example by
42/// using 16 bits of the SWAP script root.
43/// - encoding the SWAP pair, for example by using 8 bits of the offered asset faucet ID and 8 bits
44/// of the requested asset faucet ID.
45///
46/// This allows clients to search for a public SWAP note that trades USDC against ETH only through
47/// the note tag. Since tags are not validated in any way and only act as best-effort filters,
48/// further local filtering is almost always necessary. For example, there could easily be a
49/// collision on the 8 bits used in SWAP tag's faucet IDs.
50///
51/// ## Privacy vs Efficiency
52///
53/// Using note tags strikes a balance between privacy and efficiency. Without tags, querying a
54/// specific note ID reveals a user's interest to the node. Conversely, downloading and filtering
55/// all registered notes locally is highly inefficient. Tags allow users to adjust their level of
56/// privacy by choosing how broadly or narrowly they define their search criteria, letting them find
57/// the right balance between revealing too much information and incurring excessive computational
58/// overhead.
59#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Default)]
60pub struct NoteTag(u32);
61
62impl NoteTag {
63 // CONSTANTS
64 // --------------------------------------------------------------------------------------------
65
66 /// The default note tag length for an account ID with local execution.
67 pub const DEFAULT_ACCOUNT_TARGET_TAG_LENGTH: u8 = 14;
68 /// The maximum number of bits that can be encoded into the tag for local accounts.
69 pub const MAX_ACCOUNT_TARGET_TAG_LENGTH: u8 = 32;
70
71 // CONSTRUCTORS
72 // --------------------------------------------------------------------------------------------
73
74 /// Creates a new [`NoteTag`] from an arbitrary `u32`.
75 pub const fn new(tag: u32) -> Self {
76 Self(tag)
77 }
78
79 /// Constructs a note tag that targets the given `account_id`.
80 ///
81 /// The tag is a u32 constructed by taking the [`NoteTag::DEFAULT_ACCOUNT_TARGET_TAG_LENGTH`]
82 /// most significant bits of the account ID prefix and setting the remaining bits to zero.
83 pub fn with_account_target(account_id: AccountId) -> Self {
84 Self::with_custom_account_target(account_id, Self::DEFAULT_ACCOUNT_TARGET_TAG_LENGTH)
85 .expect("default account target tag length must be valid")
86 }
87
88 /// Constructs a note tag that targets the given `account_id` with a custom `tag_len`.
89 ///
90 /// The tag is a u32 constructed by taking the `tag_len` most significant bits of the account ID
91 /// prefix and setting the remaining bits to zero.
92 ///
93 /// # Errors
94 ///
95 /// Returns an error if `tag_len` is larger than [`NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH`].
96 pub fn with_custom_account_target(
97 account_id: AccountId,
98 tag_len: u8,
99 ) -> Result<Self, NoteError> {
100 if tag_len > Self::MAX_ACCOUNT_TARGET_TAG_LENGTH {
101 return Err(NoteError::NoteTagLengthTooLarge(tag_len));
102 }
103
104 let prefix = account_id.prefix().as_u64();
105 // Get the high bits as a u32.
106 let high_bits = (prefix >> 32) as u32;
107 // Create a mask that zeros out the lower 32 - len bits.
108 let mask = u32::MAX.checked_shl(u32::BITS - tag_len as u32).unwrap_or(0);
109 let tag = high_bits & mask;
110 Ok(Self(tag))
111 }
112
113 // PUBLIC ACCESSORS
114 // --------------------------------------------------------------------------------------------
115
116 /// Returns the inner u32 value of this tag.
117 pub fn as_u32(&self) -> u32 {
118 self.0
119 }
120}
121
122impl fmt::Display for NoteTag {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 write!(f, "{}", self.as_u32())
125 }
126}
127
128// CONVERSIONS INTO NOTE TAG
129// ================================================================================================
130
131impl From<u32> for NoteTag {
132 fn from(tag: u32) -> Self {
133 Self::new(tag)
134 }
135}
136
137// CONVERSIONS FROM NOTE TAG
138// ================================================================================================
139
140impl From<NoteTag> for u32 {
141 fn from(tag: NoteTag) -> Self {
142 tag.as_u32()
143 }
144}
145
146impl From<NoteTag> for Felt {
147 fn from(tag: NoteTag) -> Self {
148 Felt::from(tag.as_u32())
149 }
150}
151
152// SERIALIZATION
153// ================================================================================================
154
155impl Serializable for NoteTag {
156 fn write_into<W: ByteWriter>(&self, target: &mut W) {
157 self.as_u32().write_into(target);
158 }
159
160 fn get_size_hint(&self) -> usize {
161 core::mem::size_of::<u32>()
162 }
163}
164
165impl Deserializable for NoteTag {
166 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
167 let tag = u32::read_from(source)?;
168 Ok(Self::new(tag))
169 }
170}
171
172// TESTS
173// ================================================================================================
174
175#[cfg(test)]
176mod tests {
177
178 use super::NoteTag;
179 use crate::account::{AccountId, AccountType};
180 use crate::testing::account_id::{
181 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
182 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
183 ACCOUNT_ID_PRIVATE_SENDER,
184 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
185 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
186 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
187 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
188 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
189 ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
190 ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
191 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
192 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2,
193 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE,
194 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE_ON_CHAIN_2,
195 ACCOUNT_ID_SENDER,
196 AccountIdBuilder,
197 };
198
199 #[test]
200 fn from_account_id() {
201 let private_accounts = [
202 AccountId::try_from(ACCOUNT_ID_SENDER).unwrap(),
203 AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap(),
204 AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE).unwrap(),
205 AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap(),
206 AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap(),
207 AccountIdBuilder::new()
208 .account_type(AccountType::Private)
209 .build_with_seed([2; 32]),
210 ];
211 let public_accounts = [
212 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
213 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2).unwrap(),
214 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE).unwrap(),
215 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE_ON_CHAIN_2)
216 .unwrap(),
217 AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(),
218 AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).unwrap(),
219 AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2).unwrap(),
220 AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3).unwrap(),
221 AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap(),
222 AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1).unwrap(),
223 AccountIdBuilder::new()
224 .account_type(AccountType::Public)
225 .build_with_seed([3; 32]),
226 ];
227 for account_id in private_accounts.iter().chain(public_accounts.iter()) {
228 let tag = NoteTag::with_account_target(*account_id);
229 assert_eq!(tag.as_u32() << 14, 0, "18 least significant bits should be zero");
230 // The expected tag is the account ID prefix with the 18 least significant bits masked
231 // out, leaving the 14 most significant bits.
232 let expected = ((account_id.prefix().as_u64() >> 32) as u32)
233 & 0b1111_1111_1111_1100_0000_0000_0000_0000;
234
235 assert_eq!(tag.as_u32(), expected);
236 }
237 }
238
239 #[test]
240 fn from_custom_account_target() -> anyhow::Result<()> {
241 let account_id = AccountId::try_from(ACCOUNT_ID_SENDER)?;
242
243 let tag = NoteTag::with_custom_account_target(
244 account_id,
245 NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH,
246 )?;
247
248 assert_eq!(
249 (account_id.prefix().as_u64() >> 32) as u32,
250 tag.as_u32(),
251 "32 most significant bits should match"
252 );
253
254 Ok(())
255 }
256}