Skip to main content

miden_testing/
utils.rs

1use alloc::string::String;
2use alloc::vec::Vec;
3
4use miden_processor::crypto::RpoRandomCoin;
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::testing::storage::prepare_assets;
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#[macro_export]
19macro_rules! assert_execution_error {
20    ($execution_result:expr, $expected_err:expr) => {
21        match $execution_result {
22            Err(miden_processor::ExecutionError::FailedAssertion { label: _, source_file: _, clk: _, err_code, err_msg, err: _ }) => {
23                if let Some(ref msg) = err_msg {
24                  assert_eq!(msg.as_ref(), $expected_err.message(), "error messages did not match");
25                }
26
27                assert_eq!(
28                    err_code, $expected_err.code(),
29                    "Execution failed on assertion with an unexpected error (Actual code: {}, msg: {}, Expected code: {}).",
30                    err_code, err_msg.as_ref().map(|string| string.as_ref()).unwrap_or("<no message>"), $expected_err,
31                );
32            },
33            Ok(_) => panic!("Execution was unexpectedly successful"),
34            Err(err) => panic!("Execution error was not as expected: {err}"),
35        }
36    };
37}
38
39#[macro_export]
40macro_rules! assert_transaction_executor_error {
41    ($execution_result:expr, $expected_err:expr) => {
42        match $execution_result {
43            Err(miden_tx::TransactionExecutorError::TransactionProgramExecutionFailed(
44                miden_processor::ExecutionError::FailedAssertion {
45                    label: _,
46                    source_file: _,
47                    clk: _,
48                    err_code,
49                    err_msg,
50                    err: _,
51                },
52            )) => {
53                if let Some(ref msg) = err_msg {
54                  assert_eq!(msg.as_ref(), $expected_err.message(), "error messages did not match");
55                }
56
57                assert_eq!(
58                  err_code, $expected_err.code(),
59                  "Execution failed on assertion with an unexpected error (Actual code: {}, msg: {}, Expected: {}).",
60                  err_code, err_msg.as_ref().map(|string| string.as_ref()).unwrap_or("<no message>"), $expected_err);
61            },
62            Ok(_) => panic!("Execution was unexpectedly successful"),
63            Err(err) => panic!("Execution error was not as expected: {err}"),
64        }
65    };
66}
67
68// HELPER NOTES
69// ================================================================================================
70
71/// Creates a public `P2ANY` note.
72///
73/// A `P2ANY` note carries `assets` and a script that moves the assets into the executing account's
74/// vault.
75///
76/// The created note does not require authentication and can be consumed by any account.
77pub fn create_public_p2any_note(
78    sender: AccountId,
79    assets: impl IntoIterator<Item = Asset>,
80) -> Note {
81    let mut rng = RpoRandomCoin::new(Default::default());
82    create_p2any_note(sender, NoteType::Public, assets, &mut rng)
83}
84
85/// Creates a `P2ANY` note.
86///
87/// A `P2ANY` note carries `assets` and a script that moves the assets into the executing account's
88/// vault.
89///
90/// The created note does not require authentication and can be consumed by any account.
91pub fn create_p2any_note(
92    sender: AccountId,
93    note_type: NoteType,
94    assets: impl IntoIterator<Item = Asset>,
95    rng: &mut RpoRandomCoin,
96) -> Note {
97    let serial_number = rng.draw_word();
98    let assets: Vec<_> = assets.into_iter().collect();
99    let mut code_body = String::new();
100    for i in 0..assets.len() {
101        if i == 0 {
102            // first asset (dest_ptr is already on stack)
103            code_body.push_str(
104                "
105                # add first asset
106
107                padw dup.4 mem_loadw_be
108                padw swapw padw padw swapdw
109                call.wallet::receive_asset
110                dropw movup.12
111                # => [dest_ptr, pad(12)]
112                ",
113            );
114        } else {
115            code_body.push_str(
116                "
117                # add next asset
118
119                add.4 dup movdn.13
120                padw movup.4 mem_loadw_be
121                call.wallet::receive_asset
122                dropw movup.12
123                # => [dest_ptr, pad(12)]",
124            );
125        }
126    }
127    code_body.push_str("dropw dropw dropw dropw");
128
129    let code = format!(
130        r#"
131        use mock::account
132        use miden::protocol::active_note
133        use miden::standards::wallets::basic->wallet
134
135        begin
136            # fetch pointer & number of assets
137            push.0 exec.active_note::get_assets     # [num_assets, dest_ptr]
138
139            # runtime-check we got the expected count
140            push.{num_assets} assert_eq.err="unexpected number of assets"             # [dest_ptr]
141
142            {code_body}
143            dropw dropw dropw dropw
144        end
145        "#,
146        num_assets = assets.len(),
147    );
148
149    NoteBuilder::new(sender, SmallRng::from_seed([0; 32]))
150        .add_assets(assets.iter().copied())
151        .note_type(note_type)
152        .serial_number(serial_number)
153        .code(code)
154        .dynamically_linked_libraries(CodeBuilder::mock_libraries())
155        .build()
156        .expect("generated note script should compile")
157}
158
159/// Creates a `SPAWN` note.
160///
161///  A `SPAWN` note contains a note script that creates all `output_notes` that get passed as a
162///  parameter.
163///
164/// # Errors
165///
166/// Returns an error if:
167/// - the sender account ID of the provided output notes is not consistent or does not match the
168///   transaction's sender.
169pub fn create_spawn_note<'note, I>(
170    output_notes: impl IntoIterator<Item = &'note Note, IntoIter = I>,
171) -> anyhow::Result<Note>
172where
173    I: ExactSizeIterator<Item = &'note Note>,
174{
175    let mut output_notes = output_notes.into_iter().peekable();
176    if output_notes.len() == 0 {
177        anyhow::bail!("at least one output note is needed to create a SPAWN note");
178    }
179
180    let sender_id = output_notes
181        .peek()
182        .expect("at least one output note should be present")
183        .metadata()
184        .sender();
185
186    let note_code = note_script_that_creates_notes(sender_id, output_notes)?;
187
188    let note = NoteBuilder::new(sender_id, SmallRng::from_os_rng())
189        .code(note_code)
190        .dynamically_linked_libraries(CodeBuilder::mock_libraries())
191        .build()?;
192
193    Ok(note)
194}
195
196/// Returns the code for a note that creates all notes in `output_notes`
197fn note_script_that_creates_notes<'note>(
198    sender_id: AccountId,
199    output_notes: impl Iterator<Item = &'note Note>,
200) -> anyhow::Result<String> {
201    let mut out = String::from("use miden::protocol::output_note\n\nbegin\n");
202
203    for (idx, note) in output_notes.into_iter().enumerate() {
204        anyhow::ensure!(
205            note.metadata().sender() == sender_id,
206            "sender IDs of output notes passed to SPAWN note are inconsistent"
207        );
208
209        // Make sure that the transaction's native account matches the note sender.
210        out.push_str(&format!(
211            r#"exec.::miden::protocol::native_account::get_id
212             # => [native_account_id_prefix, native_account_id_suffix]
213             push.{sender_prefix} assert_eq.err="sender ID prefix does not match native account ID's prefix"
214             # => [native_account_id_suffix]
215             push.{sender_suffix} assert_eq.err="sender ID suffix does not match native account ID's suffix"
216             # => []
217        "#,
218          sender_prefix = sender_id.prefix().as_felt(),
219          sender_suffix = sender_id.suffix()
220        ));
221
222        if idx == 0 {
223            out.push_str("padw padw\n");
224        } else {
225            out.push_str("dropw dropw dropw\n");
226        }
227        out.push_str(&format!(
228            "
229            push.{recipient}
230            push.{note_type}
231            push.{tag}
232            exec.output_note::create\n",
233            recipient = note.recipient().digest(),
234            note_type = note.metadata().note_type() as u8,
235            tag = note.metadata().tag(),
236        ));
237
238        out.push_str(&format!(
239            "
240          push.{ATTACHMENT}
241          push.{attachment_scheme}
242          push.{attachment_kind}
243          dup.6
244          # => [note_idx, attachment_kind, attachment_scheme, ATTACHMENT, note_idx]
245          exec.output_note::set_attachment
246          # => [note_idx]
247        ",
248            ATTACHMENT = note.metadata().to_attachment_word(),
249            attachment_scheme = note.metadata().attachment().attachment_scheme().as_u32(),
250            attachment_kind = note.metadata().attachment().content().attachment_kind().as_u8(),
251        ));
252
253        let assets_str = prepare_assets(note.assets());
254        for asset in assets_str {
255            out.push_str(&format!(
256                " push.{asset}
257                  call.::miden::standards::wallets::basic::move_asset_to_note\n",
258            ));
259        }
260    }
261
262    out.push_str("repeat.5 dropw end\nend");
263
264    Ok(out)
265}