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