Skip to main content

miden_testing/
utils.rs

1use alloc::string::String;
2use alloc::vec::Vec;
3
4use miden_processor::crypto::random::RandomCoin;
5use miden_protocol::Word;
6use miden_protocol::account::AccountId;
7use miden_protocol::asset::Asset;
8use miden_protocol::crypto::rand::FeltRng;
9use miden_protocol::errors::NoteError;
10use miden_protocol::note::{Note, NoteAssets, NoteTag, NoteType, PartialNoteMetadata};
11use miden_protocol::vm::AdviceMap;
12use miden_standards::code_builder::CodeBuilder;
13use miden_standards::note::P2idNoteStorage;
14use miden_standards::testing::note::NoteBuilder;
15use rand::SeedableRng;
16use rand::rngs::SmallRng;
17
18// HELPER MACROS
19// ================================================================================================
20
21/// Asserts that a `Result<_, ExecError>` failed as expected.
22///
23/// Two forms:
24/// - `..., matches <pat> [if <guard>]` — matches the inner `ExecutionError` against `<pat>`. Use
25///   this for variants other than `FailedAssertion`, or to assert on a specific `err_code`.
26/// - `..., $expected` — delegates to `MasmError::matches_execution_error` (e.g. a `MasmError` error
27///   code).
28#[macro_export]
29macro_rules! assert_execution_error {
30    ($execution_result:expr, matches $pat:pat $(if $guard:expr)? $(,)?) => {
31        match $execution_result {
32            Err($crate::ExecError($pat)) $(if $guard)? => {},
33            Ok(_) => ::core::panic!(
34                "Execution was unexpectedly successful\nexpected error: {}",
35                ::core::stringify!($pat),
36            ),
37            Err(err) => ::core::panic!(
38                "Execution error did not match:\nexpected: {}\nactual: {}",
39                ::core::stringify!($pat),
40                err,
41            ),
42        }
43    };
44
45    ($execution_result:expr, $expected:expr $(,)?) => {
46        match $execution_result {
47            Err($crate::ExecError(actual)) => {
48                if !$expected.matches_execution_error(&actual) {
49                    ::core::panic!(
50                        "Execution error did not match:\nexpected: {}\nactual: {}",
51                        $expected,
52                        actual,
53                    );
54                }
55            },
56            Ok(_) => ::core::panic!(
57                "Execution was unexpectedly successful\nexpected error: {}",
58                $expected,
59            ),
60        }
61    };
62}
63
64/// Same as [`assert_execution_error!`], but for `TransactionExecutorError`.
65/// The `matches` and `$expected` arms unwrap
66/// `TransactionProgramExecutionFailed(_)` and match against the inner
67/// `ExecutionError`.
68#[macro_export]
69macro_rules! assert_transaction_executor_error {
70    ($execution_result:expr, matches $pat:pat $(if $guard:expr)? $(,)?) => {
71        match $execution_result {
72            Err(miden_tx::TransactionExecutorError::TransactionProgramExecutionFailed($pat))
73                $(if $guard)? => {},
74            Ok(_) => ::core::panic!(
75                "Execution was unexpectedly successful\nexpected error: {}",
76                ::core::stringify!($pat),
77            ),
78            Err(err) => ::core::panic!(
79                "Execution error did not match:\nexpected: {}\nactual: {}",
80                ::core::stringify!($pat),
81                err,
82            ),
83        }
84    };
85
86    ($execution_result:expr, $expected:expr $(,)?) => {
87        match $execution_result {
88            Err(miden_tx::TransactionExecutorError::TransactionProgramExecutionFailed(actual)) => {
89                if !$expected.matches_execution_error(&actual) {
90                    ::core::panic!(
91                        "Execution error did not match:\nexpected: {}\nactual: {}",
92                        $expected,
93                        actual,
94                    );
95                }
96            },
97            Ok(_) => ::core::panic!(
98                "Execution was unexpectedly successful\nexpected error: {}",
99                $expected,
100            ),
101            Err(err) => ::core::panic!(
102                "Execution error did not match:\nexpected: {}\nactual: {}",
103                $expected,
104                err,
105            ),
106        }
107    };
108}
109
110// HELPER NOTES
111// ================================================================================================
112
113/// Creates a public `P2ANY` note.
114///
115/// A `P2ANY` note carries `assets` and a script that moves the assets into the executing account's
116/// vault.
117///
118/// The created note does not require authentication and can be consumed by any account.
119pub fn create_public_p2any_note(
120    sender: AccountId,
121    assets: impl IntoIterator<Item = Asset>,
122) -> Note {
123    let mut rng = RandomCoin::new(Default::default());
124    create_p2any_note(sender, NoteType::Public, assets, &mut rng)
125}
126
127/// Creates a `P2ANY` note.
128///
129/// A `P2ANY` note carries `assets` and a script that moves the assets into the executing account's
130/// vault.
131///
132/// The created note does not require authentication and can be consumed by any account.
133pub fn create_p2any_note(
134    sender: AccountId,
135    note_type: NoteType,
136    assets: impl IntoIterator<Item = Asset>,
137    rng: &mut RandomCoin,
138) -> Note {
139    let serial_number = rng.draw_word();
140    let assets: Vec<_> = assets.into_iter().collect();
141    let mut code_body = String::new();
142    for asset_idx in 0..assets.len() {
143        code_body.push_str(&format!(
144            "
145                # => [dest_ptr]
146
147                # current_asset_ptr = dest_ptr + ASSET_SIZE * asset_idx
148                dup push.ASSET_SIZE mul.{asset_idx}
149                # => [current_asset_ptr, dest_ptr]
150
151                padw dup.4 add.ASSET_VALUE_MEMORY_OFFSET mem_loadw_le
152                # => [ASSET_VALUE, current_asset_ptr, dest_ptr]
153
154                padw movup.8 mem_loadw_le
155                # => [ASSET_KEY, ASSET_VALUE, current_asset_ptr, dest_ptr]
156
157                padw padw swapdw
158                # => [ASSET_KEY, ASSET_VALUE, pad(12), dest_ptr]
159
160                call.wallet::receive_asset
161                # => [pad(16), dest_ptr]
162
163                dropw dropw dropw dropw
164                # => [dest_ptr]
165                ",
166        ));
167    }
168    code_body.push_str("dropw dropw dropw dropw");
169
170    let code = format!(
171        r#"
172        use mock::account
173        use miden::protocol::active_note
174        use ::miden::protocol::asset::ASSET_VALUE_MEMORY_OFFSET
175        use ::miden::protocol::asset::ASSET_SIZE
176        use miden::standards::wallets::basic->wallet
177
178        @note_script
179        pub proc main
180            # fetch pointer & number of assets
181            push.0 exec.active_note::get_assets     # [num_assets]
182
183            # runtime-check we got the expected count
184            push.{num_assets} assert_eq.err="unexpected number of assets"             # []
185
186            push.0                                                       # [dest_ptr]
187
188            {code_body}
189            dropw dropw dropw dropw
190        end
191        "#,
192        num_assets = assets.len(),
193    );
194
195    NoteBuilder::new(sender, SmallRng::from_seed([0; 32]))
196        .add_assets(assets.iter().copied())
197        .note_type(note_type)
198        .serial_number(serial_number)
199        .code(code)
200        .dynamically_linked_libraries(CodeBuilder::mock_libraries())
201        .build()
202        .expect("generated note script should compile")
203}
204
205/// Creates a `SPAWN` note.
206///
207///  A `SPAWN` note contains a note script that creates all `output_notes` that get passed as a
208///  parameter.
209///
210/// # Errors
211///
212/// Returns an error if:
213/// - the sender account ID of the provided output notes is not consistent or does not match the
214///   transaction's sender.
215pub fn create_spawn_note<'note, I>(
216    output_notes: impl IntoIterator<Item = &'note Note, IntoIter = I>,
217) -> anyhow::Result<Note>
218where
219    I: ExactSizeIterator<Item = &'note Note>,
220{
221    let mut output_notes = output_notes.into_iter().peekable();
222    if output_notes.len() == 0 {
223        anyhow::bail!("at least one output note is needed to create a SPAWN note");
224    }
225
226    let sender_id = output_notes
227        .peek()
228        .expect("at least one output note should be present")
229        .metadata()
230        .sender();
231
232    let (note_code, advice_map) = note_script_that_creates_notes(sender_id, output_notes)?;
233
234    let note = NoteBuilder::new(sender_id, SmallRng::from_os_rng())
235        .code(note_code)
236        .advice_map(advice_map)
237        .dynamically_linked_libraries(CodeBuilder::mock_libraries())
238        .build()?;
239
240    Ok(note)
241}
242
243/// Returns the code for a note that creates all notes in `output_notes`, along with an
244/// advice map containing the elements for any attachments keyed by their commitment.
245fn note_script_that_creates_notes<'note>(
246    sender_id: AccountId,
247    output_notes: impl Iterator<Item = &'note Note>,
248) -> anyhow::Result<(String, AdviceMap)> {
249    let mut advice_map = AdviceMap::default();
250    let mut out = String::from("use miden::protocol::output_note\n\n@note_script\npub proc main\n");
251
252    for (idx, note) in output_notes.into_iter().enumerate() {
253        anyhow::ensure!(
254            note.metadata().sender() == sender_id,
255            "sender IDs of output notes passed to SPAWN note are inconsistent"
256        );
257
258        // Make sure that the transaction's native account matches the note sender.
259        out.push_str(&format!(
260            r#"exec.::miden::protocol::native_account::get_id
261             # => [native_account_id_suffix, native_account_id_prefix]
262             push.{sender_suffix} assert_eq.err="sender ID suffix does not match native account ID's suffix"
263             # => [native_account_id_prefix]
264             push.{sender_prefix} assert_eq.err="sender ID prefix does not match native account ID's prefix"
265             # => []
266        "#,
267          sender_prefix = sender_id.prefix().as_felt(),
268          sender_suffix = sender_id.suffix()
269        ));
270
271        if idx == 0 {
272            out.push_str("padw padw\n");
273        } else {
274            out.push_str("dropw dropw dropw\n");
275        }
276        out.push_str(&format!(
277            "
278            push.{recipient}
279            push.{note_type}
280            push.{tag}
281            exec.output_note::create\n",
282            recipient = note.recipient().digest(),
283            note_type = note.metadata().note_type() as u8,
284            tag = note.metadata().tag(),
285        ));
286
287        for attachment in note.attachments().iter() {
288            let attachment_scheme = attachment.attachment_scheme().as_u16();
289            let commitment = attachment.content().to_commitment();
290
291            out.push_str(&format!(
292                "
293                      dup
294                      push.{commitment}
295                      push.{attachment_scheme}
296                      # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx, note_idx]
297                      exec.output_note::add_attachment
298                      # => [note_idx]
299                    ",
300            ));
301
302            // Add the elements to the advice map keyed by the commitment.
303            advice_map.insert(commitment, attachment.content().to_elements());
304        }
305
306        for asset in note.assets().iter() {
307            out.push_str(&format!(
308                " dup
309                  push.{ASSET_VALUE}
310                  push.{ASSET_KEY}
311                  # => [ASSET_KEY, ASSET_VALUE, note_idx, note_idx]
312                  call.::miden::standards::wallets::basic::move_asset_to_note
313                  # => [note_idx]
314                ",
315                ASSET_KEY = asset.to_key_word(),
316                ASSET_VALUE = asset.to_value_word(),
317            ));
318        }
319    }
320
321    out.push_str("repeat.5 dropw end\nend");
322
323    Ok((out, advice_map))
324}
325
326/// Generates a P2ID note - Pay-to-ID note with an exact serial number
327pub fn create_p2id_note_exact(
328    sender: AccountId,
329    target: AccountId,
330    assets: Vec<Asset>,
331    note_type: NoteType,
332    serial_num: Word,
333) -> Result<Note, NoteError> {
334    let recipient = P2idNoteStorage::new(target).into_recipient(serial_num);
335
336    let tag = NoteTag::with_account_target(target);
337
338    let metadata = PartialNoteMetadata::new(sender, note_type).with_tag(tag);
339    let vault = NoteAssets::new(assets)?;
340
341    Ok(Note::new(vault, metadata, recipient))
342}