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