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}