miden_lib/note/
well_known_note.rs

1use miden_objects::{
2    Digest, Felt,
3    account::AccountId,
4    assembly::{ProcedureName, QualifiedProcedureName},
5    block::BlockNumber,
6    note::{Note, NoteScript},
7    utils::{Deserializable, sync::LazyLock},
8    vm::Program,
9};
10
11use crate::account::{
12    components::basic_wallet_library,
13    interface::{AccountComponentInterface, AccountInterface, NoteAccountCompatibility},
14};
15
16// WELL KNOWN NOTE SCRIPTS
17// ================================================================================================
18
19// Initialize the P2ID note script only once
20static P2ID_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
21    let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/P2ID.masb"));
22    let program = Program::read_from_bytes(bytes).expect("Shipped P2ID script is well-formed");
23    NoteScript::new(program)
24});
25
26// Initialize the P2IDE note script only once
27static P2IDE_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
28    let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/P2IDE.masb"));
29    let program = Program::read_from_bytes(bytes).expect("Shipped P2IDE script is well-formed");
30    NoteScript::new(program)
31});
32
33// Initialize the SWAP note script only once
34static SWAP_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
35    let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/SWAP.masb"));
36    let program = Program::read_from_bytes(bytes).expect("Shipped SWAP script is well-formed");
37    NoteScript::new(program)
38});
39
40/// Returns the P2ID (Pay-to-ID) note script.
41fn p2id() -> NoteScript {
42    P2ID_SCRIPT.clone()
43}
44
45/// Returns the P2ID (Pay-to-ID) note script root.
46fn p2id_root() -> Digest {
47    P2ID_SCRIPT.root()
48}
49
50/// Returns the P2IDE (Pay-to-ID with optional reclaim & timelock) note script.
51fn p2ide() -> NoteScript {
52    P2IDE_SCRIPT.clone()
53}
54
55/// Returns the P2IDE (Pay-to-ID with optional reclaim & timelock) note script root.
56fn p2ide_root() -> Digest {
57    P2IDE_SCRIPT.root()
58}
59
60/// Returns the SWAP (Swap note) note script.
61fn swap() -> NoteScript {
62    SWAP_SCRIPT.clone()
63}
64
65/// Returns the SWAP (Swap note) note script root.
66fn swap_root() -> Digest {
67    SWAP_SCRIPT.root()
68}
69
70// WELL KNOWN NOTE
71// ================================================================================================
72
73/// The enum holding the types of basic well-known notes provided by the `miden-lib`.
74pub enum WellKnownNote {
75    P2ID,
76    P2IDE,
77    SWAP,
78}
79
80impl WellKnownNote {
81    // CONSTANTS
82    // --------------------------------------------------------------------------------------------
83
84    /// Expected number of inputs of the P2ID note.
85    const P2ID_NUM_INPUTS: usize = 2;
86
87    /// Expected number of inputs of the P2IDE note.
88    const P2IDE_NUM_INPUTS: usize = 4;
89
90    /// Expected number of inputs of the SWAP note.
91    const SWAP_NUM_INPUTS: usize = 10;
92
93    // CONSTRUCTOR
94    // --------------------------------------------------------------------------------------------
95
96    /// Returns a [WellKnownNote] instance based on the note script of the provided [Note]. Returns
97    /// `None` if the provided note is not a basic well-known note.
98    pub fn from_note(note: &Note) -> Option<Self> {
99        let note_script_root = note.script().root();
100
101        if note_script_root == p2id_root() {
102            return Some(Self::P2ID);
103        }
104        if note_script_root == p2ide_root() {
105            return Some(Self::P2IDE);
106        }
107        if note_script_root == swap_root() {
108            return Some(Self::SWAP);
109        }
110
111        None
112    }
113
114    // PUBLIC ACCESSORS
115    // --------------------------------------------------------------------------------------------
116
117    /// Returns the expected inputs number of the current note.
118    pub fn num_expected_inputs(&self) -> usize {
119        match self {
120            Self::P2ID => Self::P2ID_NUM_INPUTS,
121            Self::P2IDE => Self::P2IDE_NUM_INPUTS,
122            Self::SWAP => Self::SWAP_NUM_INPUTS,
123        }
124    }
125
126    /// Returns the note script of the current [WellKnownNote] instance.
127    pub fn script(&self) -> NoteScript {
128        match self {
129            Self::P2ID => p2id(),
130            Self::P2IDE => p2ide(),
131            Self::SWAP => swap(),
132        }
133    }
134
135    /// Returns the script root of the current [WellKnownNote] instance.
136    pub fn script_root(&self) -> Digest {
137        match self {
138            Self::P2ID => p2id_root(),
139            Self::P2IDE => p2ide_root(),
140            Self::SWAP => swap_root(),
141        }
142    }
143
144    /// Returns a boolean value indicating whether this [WellKnownNote] is compatible with the
145    /// provided [AccountInterface].
146    pub fn is_compatible_with(&self, account_interface: &AccountInterface) -> bool {
147        if account_interface.components().contains(&AccountComponentInterface::BasicWallet) {
148            return true;
149        }
150
151        let interface_proc_digests = account_interface.get_procedure_digests();
152        match self {
153            Self::P2ID | &Self::P2IDE => {
154                // Get the hash of the "receive_asset" procedure and check that this procedure is
155                // presented in the provided account interfaces. P2ID and P2IDE notes requires only
156                // this procedure to be consumed by the account.
157                let receive_asset_proc_name = QualifiedProcedureName::new(
158                    Default::default(),
159                    ProcedureName::new("receive_asset").unwrap(),
160                );
161                let node_id = basic_wallet_library().get_export_node_id(&receive_asset_proc_name);
162                let receive_asset_digest = basic_wallet_library().mast_forest()[node_id].digest();
163
164                interface_proc_digests.contains(&receive_asset_digest)
165            },
166            Self::SWAP => {
167                // Make sure that all procedures from the basic wallet library are presented in the
168                // provided account interfaces. SWAP note requires the whole basic wallet interface
169                // to be consumed by the account.
170                basic_wallet_library()
171                    .mast_forest()
172                    .procedure_digests()
173                    .all(|proc_digest| interface_proc_digests.contains(&proc_digest))
174            },
175        }
176    }
177
178    /// Checks the correctness of the provided note inputs against the target account.
179    ///
180    /// It performs:
181    /// - for all notes: a check that note inputs have correct number of values.
182    /// - for `P2ID` note: assertion that the account ID provided by the note inputs is equal to the
183    ///   target account ID.
184    /// - for `P2IDE` note:
185    ///   - assertion that the ID of the account, against which the transaction is being executed,
186    ///     is equal to the target account ID specified in the note inputs (which means that the
187    ///     note is going to be consumed by the target account) or is equal to the ID of the
188    ///     account, which sent this note (which means that the note is going to be consumed by the
189    ///     sender account).
190    ///   - assertion that the timelock height was reached.
191    ///   - assertion that the reclaim height was reached.
192    pub fn check_note_inputs(
193        &self,
194        note: &Note,
195        target_account_id: AccountId,
196        block_ref: BlockNumber,
197    ) -> NoteAccountCompatibility {
198        match self {
199            WellKnownNote::P2ID => {
200                let note_inputs = note.inputs().values();
201                if note_inputs.len() != self.num_expected_inputs() {
202                    return NoteAccountCompatibility::No;
203                }
204
205                // Return `No` if the note input values used to construct the account ID are invalid
206                let Some(input_account_id) = try_read_account_id_from_inputs(note_inputs) else {
207                    return NoteAccountCompatibility::No;
208                };
209
210                // check that the account ID in the note inputs equal to the target account ID
211                if input_account_id == target_account_id {
212                    NoteAccountCompatibility::Yes
213                } else {
214                    NoteAccountCompatibility::No
215                }
216            },
217            WellKnownNote::P2IDE => {
218                let note_inputs = note.inputs().values();
219
220                // check expected number of inputs
221                if note_inputs.len() != self.num_expected_inputs() {
222                    return NoteAccountCompatibility::No;
223                }
224
225                // parse timelock height and enforce it
226                let Ok(timelock_height) = note_inputs[3].try_into() else {
227                    return NoteAccountCompatibility::No;
228                };
229                if block_ref.as_u32() < timelock_height {
230                    return NoteAccountCompatibility::No; // still locked
231                }
232
233                // identify who is trying to spend
234                let Some(input_account_id) = try_read_account_id_from_inputs(note_inputs) else {
235                    return NoteAccountCompatibility::No;
236                };
237                let sender_account_id = note.metadata().sender();
238                let is_target = input_account_id == target_account_id;
239                let is_sender = target_account_id == sender_account_id;
240
241                if is_target {
242                    // target (possibly also the sender) can spend as soon as the timelock is over
243                    NoteAccountCompatibility::Yes
244                } else if is_sender {
245                    // sender can reclaim only after reclaim height
246                    let Ok(reclaim_height) = note_inputs[2].try_into() else {
247                        return NoteAccountCompatibility::No;
248                    };
249                    return if block_ref.as_u32() >= reclaim_height {
250                        NoteAccountCompatibility::Yes
251                    } else {
252                        NoteAccountCompatibility::No
253                    };
254                } else {
255                    // neither target nor sender
256                    NoteAccountCompatibility::No
257                }
258            },
259
260            WellKnownNote::SWAP => {
261                if note.inputs().values().len() != self.num_expected_inputs() {
262                    return NoteAccountCompatibility::No;
263                }
264
265                NoteAccountCompatibility::Maybe
266            },
267        }
268    }
269}
270
271// HELPER FUNCTIONS
272// ================================================================================================
273
274/// Reads the account ID from the first two note input values.
275///
276/// Returns None if the note input values used to construct the account ID are invalid.
277fn try_read_account_id_from_inputs(note_inputs: &[Felt]) -> Option<AccountId> {
278    let account_id_felts: [Felt; 2] = note_inputs[0..2].try_into().expect(
279        "Should be able to convert the first two note inputs to an array of two Felt elements",
280    );
281
282    AccountId::try_from([account_id_felts[1], account_id_felts[0]]).ok()
283}