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