Skip to main content

miden_testing/
utils.rs

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