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};
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, AccountStorageMode};
180    use crate::testing::account_id::{
181        ACCOUNT_ID_NETWORK_FUNGIBLE_FAUCET,
182        ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET,
183        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
184        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
185        ACCOUNT_ID_PRIVATE_SENDER,
186        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
187        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
188        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2,
189        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
190        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
191        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1,
192        ACCOUNT_ID_REGULAR_NETWORK_ACCOUNT_IMMUTABLE_CODE,
193        ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
194        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
195        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2,
196        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE,
197        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE_ON_CHAIN_2,
198        ACCOUNT_ID_SENDER,
199        AccountIdBuilder,
200    };
201
202    #[test]
203    fn from_account_id() {
204        let private_accounts = [
205            AccountId::try_from(ACCOUNT_ID_SENDER).unwrap(),
206            AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap(),
207            AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE).unwrap(),
208            AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap(),
209            AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap(),
210            AccountIdBuilder::new()
211                .storage_mode(AccountStorageMode::Private)
212                .build_with_seed([2; 32]),
213        ];
214        let public_accounts = [
215            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap(),
216            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE_2).unwrap(),
217            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE).unwrap(),
218            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE_ON_CHAIN_2)
219                .unwrap(),
220            AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(),
221            AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).unwrap(),
222            AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2).unwrap(),
223            AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3).unwrap(),
224            AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap(),
225            AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1).unwrap(),
226            AccountIdBuilder::new()
227                .storage_mode(AccountStorageMode::Public)
228                .build_with_seed([3; 32]),
229        ];
230        let network_accounts = [
231            AccountId::try_from(ACCOUNT_ID_REGULAR_NETWORK_ACCOUNT_IMMUTABLE_CODE).unwrap(),
232            AccountId::try_from(ACCOUNT_ID_NETWORK_FUNGIBLE_FAUCET).unwrap(),
233            AccountId::try_from(ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET).unwrap(),
234            AccountIdBuilder::new()
235                .storage_mode(AccountStorageMode::Network)
236                .build_with_seed([4; 32]),
237        ];
238
239        for account_id in private_accounts
240            .iter()
241            .chain(public_accounts.iter())
242            .chain(network_accounts.iter())
243        {
244            let tag = NoteTag::with_account_target(*account_id);
245            assert_eq!(tag.as_u32() << 16, 0, "16 least significant bits should be zero");
246            let expected = ((account_id.prefix().as_u64() >> 32) as u32) >> 16;
247            let actual = tag.as_u32() >> 16;
248
249            assert_eq!(actual, expected, "14 most significant bits should match");
250        }
251    }
252
253    #[test]
254    fn from_custom_account_target() -> anyhow::Result<()> {
255        let account_id = AccountId::try_from(ACCOUNT_ID_SENDER)?;
256
257        let tag = NoteTag::with_custom_account_target(
258            account_id,
259            NoteTag::MAX_ACCOUNT_TARGET_TAG_LENGTH,
260        )?;
261
262        assert_eq!(
263            (account_id.prefix().as_u64() >> 32) as u32,
264            tag.as_u32(),
265            "32 most significant bits should match"
266        );
267
268        Ok(())
269    }
270}