miden_testing/
utils.rs

1use alloc::string::String;
2use alloc::vec::Vec;
3
4use miden_lib::testing::note::NoteBuilder;
5use miden_lib::transaction::TransactionKernel;
6use miden_objects::account::AccountId;
7use miden_objects::asset::Asset;
8use miden_objects::crypto::rand::FeltRng;
9use miden_objects::note::{Note, NoteType};
10use miden_objects::testing::storage::prepare_assets;
11use miden_processor::Felt;
12use miden_processor::crypto::RpoRandomCoin;
13use rand::SeedableRng;
14use rand::rngs::SmallRng;
15
16// HELPER MACROS
17// ================================================================================================
18
19#[macro_export]
20macro_rules! assert_execution_error {
21    ($execution_result:expr, $expected_err:expr) => {
22        match $execution_result {
23            Err(miden_processor::ExecutionError::FailedAssertion { label: _, source_file: _, clk: _, err_code, err_msg }) => {
24                if let Some(ref msg) = err_msg {
25                  assert_eq!(msg.as_ref(), $expected_err.message(), "error messages did not match");
26                }
27
28                assert_eq!(
29                    err_code, $expected_err.code(),
30                    "Execution failed on assertion with an unexpected error (Actual code: {}, msg: {}, Expected code: {}).",
31                    err_code, err_msg.as_ref().map(|string| string.as_ref()).unwrap_or("<no message>"), $expected_err,
32                );
33            },
34            Ok(_) => panic!("Execution was unexpectedly successful"),
35            Err(err) => panic!("Execution error was not as expected: {err}"),
36        }
37    };
38}
39
40#[macro_export]
41macro_rules! assert_transaction_executor_error {
42    ($execution_result:expr, $expected_err:expr) => {
43        match $execution_result {
44            Err(miden_tx::TransactionExecutorError::TransactionProgramExecutionFailed(
45                miden_processor::ExecutionError::FailedAssertion {
46                    label: _,
47                    source_file: _,
48                    clk: _,
49                    err_code,
50                    err_msg,
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        "
131        use.mock::account
132        use.miden::active_note
133        use.miden::contracts::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             # [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(TransactionKernel::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(TransactionKernel::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::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::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.{hint}
231            push.{note_type}
232            push.{aux}
233            push.{tag}
234            call.output_note::create\n",
235            recipient = note.recipient().digest(),
236            hint = Felt::from(note.metadata().execution_hint()),
237            note_type = note.metadata().note_type() as u8,
238            aux = note.metadata().aux(),
239            tag = note.metadata().tag(),
240        ));
241
242        let assets_str = prepare_assets(note.assets());
243        for asset in assets_str {
244            out.push_str(&format!(
245                " push.{asset}
246                  call.::miden::contracts::wallets::basic::move_asset_to_note\n",
247            ));
248        }
249    }
250
251    out.push_str("repeat.5 dropw end\nend");
252
253    Ok(out)
254}