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