Skip to main content

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}