Skip to main content

miden_standards/note/
p2ide.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::assembly::Path;
5use miden_protocol::asset::Asset;
6use miden_protocol::block::BlockNumber;
7use miden_protocol::crypto::rand::FeltRng;
8use miden_protocol::errors::NoteError;
9use miden_protocol::note::{
10    Note,
11    NoteAssets,
12    NoteAttachments,
13    NoteRecipient,
14    NoteScript,
15    NoteScriptRoot,
16    NoteStorage,
17    NoteTag,
18    NoteType,
19    PartialNoteMetadata,
20};
21use miden_protocol::utils::sync::LazyLock;
22use miden_protocol::{Felt, Word};
23
24use crate::StandardsLib;
25// NOTE SCRIPT
26// ================================================================================================
27
28/// Path to the P2IDE note script procedure in the standards library.
29const P2IDE_SCRIPT_PATH: &str = "::miden::standards::notes::p2ide::main";
30
31// Initialize the P2IDE note script only once
32static P2IDE_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
33    let standards_lib = StandardsLib::default();
34    let path = Path::new(P2IDE_SCRIPT_PATH);
35    NoteScript::from_library_reference(standards_lib.as_ref(), path)
36        .expect("Standards library contains P2IDE note script procedure")
37});
38
39// P2IDE NOTE
40// ================================================================================================
41
42/// Pay-to-ID Extended (P2IDE) note abstraction.
43///
44/// A P2IDE note enables transferring assets to a target account specified in the note storage.
45/// The note may optionally include:
46///
47/// - A reclaim height allowing the sender to recover assets if the note remains unconsumed
48/// - A timelock height preventing consumption before a given block
49///
50/// These constraints are encoded in `P2ideNoteStorage` and enforced by the associated note script.
51pub struct P2ideNote;
52
53impl P2ideNote {
54    // CONSTANTS
55    // --------------------------------------------------------------------------------------------
56
57    /// Expected number of storage items of the P2IDE note.
58    pub const NUM_STORAGE_ITEMS: usize = P2ideNoteStorage::NUM_ITEMS;
59
60    // PUBLIC ACCESSORS
61    // --------------------------------------------------------------------------------------------
62
63    /// Returns the script of the P2IDE (Pay-to-ID extended) note.
64    pub fn script() -> NoteScript {
65        P2IDE_SCRIPT.clone()
66    }
67
68    /// Returns the P2IDE (Pay-to-ID extended) note script root.
69    pub fn script_root() -> NoteScriptRoot {
70        P2IDE_SCRIPT.root()
71    }
72
73    // BUILDERS
74    // --------------------------------------------------------------------------------------------
75
76    /// Generates a P2IDE note using the provided storage configuration.
77    ///
78    /// The note recipient and execution constraints are derived from
79    /// `P2ideNoteStorage`. A random serial number is generated using `rng`,
80    /// and the note tag is set to the storage target account.
81    ///
82    /// # Errors
83    /// Returns an error if construction of the note recipient or asset vault fails.
84    pub fn create<R: FeltRng>(
85        sender: AccountId,
86        storage: P2ideNoteStorage,
87        assets: Vec<Asset>,
88        note_type: NoteType,
89        attachments: NoteAttachments,
90        rng: &mut R,
91    ) -> Result<Note, NoteError> {
92        let serial_num = rng.draw_word();
93        let recipient = storage.into_recipient(serial_num)?;
94        let tag = NoteTag::with_account_target(storage.target());
95
96        let metadata = PartialNoteMetadata::new(sender, note_type).with_tag(tag);
97        let vault = NoteAssets::new(assets)?;
98
99        Ok(Note::with_attachments(vault, metadata, recipient, attachments))
100    }
101}
102
103// P2IDE NOTE STORAGE
104// ================================================================================================
105
106/// Canonical storage representation for a P2IDE note.
107///
108/// Stores the target account ID together with optional
109/// reclaim and timelock constraints controlling when
110/// the note can be spent or reclaimed.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub struct P2ideNoteStorage {
113    pub target: AccountId,
114    pub reclaim_height: Option<BlockNumber>,
115    pub timelock_height: Option<BlockNumber>,
116}
117
118impl P2ideNoteStorage {
119    // CONSTANTS
120    // --------------------------------------------------------------------------------------------
121
122    /// Expected number of storage items of the P2IDE note.
123    pub const NUM_ITEMS: usize = 4;
124
125    /// Creates new P2IDE note storage.
126    pub fn new(
127        target: AccountId,
128        reclaim_height: Option<BlockNumber>,
129        timelock_height: Option<BlockNumber>,
130    ) -> Self {
131        Self { target, reclaim_height, timelock_height }
132    }
133
134    /// Consumes the storage and returns a P2IDE [`NoteRecipient`] with the provided serial number.
135    pub fn into_recipient(self, serial_num: Word) -> Result<NoteRecipient, NoteError> {
136        let note_script = P2ideNote::script();
137        Ok(NoteRecipient::new(serial_num, note_script, self.into()))
138    }
139
140    /// Returns the target account ID.
141    pub fn target(&self) -> AccountId {
142        self.target
143    }
144
145    /// Returns the reclaim block height (if any).
146    pub fn reclaim_height(&self) -> Option<BlockNumber> {
147        self.reclaim_height
148    }
149
150    /// Returns the timelock block height (if any).
151    pub fn timelock_height(&self) -> Option<BlockNumber> {
152        self.timelock_height
153    }
154}
155
156impl From<P2ideNoteStorage> for NoteStorage {
157    fn from(storage: P2ideNoteStorage) -> Self {
158        let reclaim = storage.reclaim_height.map(Felt::from).unwrap_or(Felt::ZERO);
159        let timelock = storage.timelock_height.map(Felt::from).unwrap_or(Felt::ZERO);
160
161        NoteStorage::new(vec![
162            storage.target.suffix(),
163            storage.target.prefix().as_felt(),
164            reclaim,
165            timelock,
166        ])
167        .expect("number of storage items should not exceed max storage items")
168    }
169}
170
171impl TryFrom<&[Felt]> for P2ideNoteStorage {
172    type Error = NoteError;
173
174    fn try_from(note_storage: &[Felt]) -> Result<Self, Self::Error> {
175        if note_storage.len() != P2ideNote::NUM_STORAGE_ITEMS {
176            return Err(NoteError::InvalidNoteStorageLength {
177                expected: P2ideNote::NUM_STORAGE_ITEMS,
178                actual: note_storage.len(),
179            });
180        }
181
182        let target = AccountId::try_from_elements(note_storage[0], note_storage[1])
183            .map_err(|err| NoteError::other_with_source("failed to create account id", err))?;
184
185        let reclaim_height = if note_storage[2] == Felt::ZERO {
186            None
187        } else {
188            let height: u32 = note_storage[2]
189                .as_canonical_u64()
190                .try_into()
191                .map_err(|e| NoteError::other_with_source("invalid note storage", e))?;
192
193            Some(BlockNumber::from(height))
194        };
195
196        let timelock_height = if note_storage[3] == Felt::ZERO {
197            None
198        } else {
199            let height: u32 = note_storage[3]
200                .as_canonical_u64()
201                .try_into()
202                .map_err(|e| NoteError::other_with_source("invalid note storage", e))?;
203
204            Some(BlockNumber::from(height))
205        };
206
207        Ok(Self { target, reclaim_height, timelock_height })
208    }
209}
210
211// TESTS
212// ================================================================================================
213
214#[cfg(test)]
215mod tests {
216    use miden_protocol::Felt;
217    use miden_protocol::account::{AccountId, AccountIdVersion, AccountType};
218    use miden_protocol::block::BlockNumber;
219    use miden_protocol::errors::NoteError;
220
221    use super::*;
222
223    fn dummy_account() -> AccountId {
224        AccountId::dummy([3u8; 15], AccountIdVersion::Version1, AccountType::Private)
225    }
226
227    #[test]
228    fn try_from_valid_storage_with_all_fields_succeeds() {
229        let target = dummy_account();
230
231        let storage = vec![
232            target.suffix(),
233            target.prefix().as_felt(),
234            Felt::from(42u32),
235            Felt::from(100u32),
236        ];
237
238        let decoded = P2ideNoteStorage::try_from(storage.as_slice())
239            .expect("valid P2IDE storage should decode");
240
241        assert_eq!(decoded.target(), target);
242        assert_eq!(decoded.reclaim_height(), Some(BlockNumber::from(42u32)));
243        assert_eq!(decoded.timelock_height(), Some(BlockNumber::from(100u32)));
244    }
245
246    #[test]
247    fn try_from_zero_heights_map_to_none() {
248        let target = dummy_account();
249
250        let storage = vec![target.suffix(), target.prefix().as_felt(), Felt::ZERO, Felt::ZERO];
251
252        let decoded = P2ideNoteStorage::try_from(storage.as_slice()).unwrap();
253
254        assert_eq!(decoded.reclaim_height(), None);
255        assert_eq!(decoded.timelock_height(), None);
256    }
257
258    #[test]
259    fn try_from_invalid_length_fails() {
260        let storage = vec![Felt::ZERO; 3];
261
262        let err =
263            P2ideNoteStorage::try_from(storage.as_slice()).expect_err("wrong length must fail");
264
265        assert!(matches!(
266            err,
267            NoteError::InvalidNoteStorageLength {
268                expected: P2ideNote::NUM_STORAGE_ITEMS,
269                actual: 3
270            }
271        ));
272    }
273
274    #[test]
275    fn try_from_invalid_account_id_fails() {
276        let storage = vec![Felt::from(999_u32), Felt::from(888_u32), Felt::ZERO, Felt::ZERO];
277
278        let err = P2ideNoteStorage::try_from(storage.as_slice())
279            .expect_err("invalid account id encoding must fail");
280
281        assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
282    }
283
284    #[test]
285    fn try_from_reclaim_height_overflow_fails() {
286        let target = dummy_account();
287
288        // > u32::MAX
289        let overflow = Felt::new_unchecked(u64::from(u32::MAX) + 1);
290
291        let storage = vec![target.suffix(), target.prefix().as_felt(), overflow, Felt::ZERO];
292
293        let err = P2ideNoteStorage::try_from(storage.as_slice())
294            .expect_err("overflow reclaim height must fail");
295
296        assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
297    }
298
299    #[test]
300    fn try_from_timelock_height_overflow_fails() {
301        let target = dummy_account();
302
303        let overflow = Felt::new_unchecked(u64::from(u32::MAX) + 10);
304
305        let storage = vec![target.suffix(), target.prefix().as_felt(), Felt::ZERO, overflow];
306
307        let err = P2ideNoteStorage::try_from(storage.as_slice())
308            .expect_err("overflow timelock height must fail");
309
310        assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
311    }
312}