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 P2IDR note script only once
27static P2IDR_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
28    let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/P2IDR.masb"));
29    let program = Program::read_from_bytes(bytes).expect("Shipped P2IDR 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 P2IDR (Pay-to-ID with recall) note script.
51fn p2idr() -> NoteScript {
52    P2IDR_SCRIPT.clone()
53}
54
55/// Returns the P2IDR (Pay-to-ID with recall) note script root.
56fn p2idr_root() -> Digest {
57    P2IDR_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    P2IDR,
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 P2IDR note.
88    const P2IDR_NUM_INPUTS: usize = 3;
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 == p2idr_root() {
105            return Some(Self::P2IDR);
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::P2IDR => Self::P2IDR_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::P2IDR => p2idr(),
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::P2IDR => p2idr_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::P2IDR => {
154                // Get the hash of the "receive_asset" procedure and check that this procedure is
155                // presented in the provided account interfaces. P2ID and P2IDR 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 `P2IDR` note: assertion that the account ID provided by the note inputs is equal to
185    ///   the target account ID (which means that the note is going to be consumed by the target
186    ///   account) or that the target account ID is equal to the sender account ID (which means that
187    ///   the note is going to be consumed by the sender account)
188    pub fn check_note_inputs(
189        &self,
190        note: &Note,
191        target_account_id: AccountId,
192        block_ref: BlockNumber,
193    ) -> NoteAccountCompatibility {
194        match self {
195            WellKnownNote::P2ID => {
196                let note_inputs = note.inputs().values();
197                if note_inputs.len() != self.num_expected_inputs() {
198                    return NoteAccountCompatibility::No;
199                }
200
201                // Return `No` if the note input values used to construct the account ID are invalid
202                let Some(input_account_id) = try_read_account_id_from_inputs(note_inputs) else {
203                    return NoteAccountCompatibility::No;
204                };
205
206                // check that the account ID in the note inputs equal to the target account ID
207                if input_account_id == target_account_id {
208                    NoteAccountCompatibility::Yes
209                } else {
210                    NoteAccountCompatibility::No
211                }
212            },
213            WellKnownNote::P2IDR => {
214                let note_inputs = note.inputs().values();
215                if note_inputs.len() != self.num_expected_inputs() {
216                    return NoteAccountCompatibility::No;
217                }
218
219                let recall_height: Result<u32, _> = note_inputs[2].try_into();
220                // Return `No` if the note input value which represents the recall height is invalid
221                let Ok(recall_height) = recall_height else {
222                    return NoteAccountCompatibility::No;
223                };
224
225                // Return `No` if the note input values used to construct the account ID are invalid
226                let Some(input_account_id) = try_read_account_id_from_inputs(note_inputs) else {
227                    return NoteAccountCompatibility::No;
228                };
229
230                if block_ref.as_u32() >= recall_height {
231                    let sender_account_id = note.metadata().sender();
232                    // if the sender can already reclaim the assets back, then:
233                    // - target account ID could be equal to the inputs account ID if the note is
234                    //   going to be consumed by the target account
235                    // - target account ID could be equal to the sender account ID if the note is
236                    //   going to be consumed by the sender account
237                    if [input_account_id, sender_account_id].contains(&target_account_id) {
238                        NoteAccountCompatibility::Yes
239                    } else {
240                        NoteAccountCompatibility::No
241                    }
242                } else {
243                    // in this case note could be consumed only by the target account
244                    if input_account_id == target_account_id {
245                        NoteAccountCompatibility::Yes
246                    } else {
247                        NoteAccountCompatibility::No
248                    }
249                }
250            },
251            WellKnownNote::SWAP => {
252                if note.inputs().values().len() != self.num_expected_inputs() {
253                    return NoteAccountCompatibility::No;
254                }
255
256                NoteAccountCompatibility::Maybe
257            },
258        }
259    }
260}
261
262// HELPER FUNCTIONS
263// ================================================================================================
264
265/// Reads the account ID from the first two note input values.
266///
267/// Returns None if the note input values used to construct the account ID are invalid.
268fn try_read_account_id_from_inputs(note_inputs: &[Felt]) -> Option<AccountId> {
269    let account_id_felts: [Felt; 2] = note_inputs[0..2].try_into().expect(
270        "Should be able to convert the first two note inputs to an array of two Felt elements",
271    );
272
273    AccountId::try_from([account_id_felts[1], account_id_felts[0]]).ok()
274}