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, NoteTag, NoteType, PartialNoteMetadata};
11use miden_protocol::vm::AdviceMap;
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::OperationError { label: _, source_file: _, err: miden_processor::operation::OperationError::FailedAssertion { err_code, err_msg } })) => {
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::OperationError {
48                    label: _,
49                    source_file: _,
50                    err: miden_processor::operation::OperationError::FailedAssertion {
51                        err_code,
52                        err_msg,
53                    },
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 = RandomCoin::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 RandomCoin,
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 asset_idx in 0..assets.len() {
104        code_body.push_str(&format!(
105            "
106                # => [dest_ptr]
107
108                # current_asset_ptr = dest_ptr + ASSET_SIZE * asset_idx
109                dup push.ASSET_SIZE mul.{asset_idx}
110                # => [current_asset_ptr, dest_ptr]
111
112                padw dup.4 add.ASSET_VALUE_MEMORY_OFFSET mem_loadw_le
113                # => [ASSET_VALUE, current_asset_ptr, dest_ptr]
114
115                padw movup.8 mem_loadw_le
116                # => [ASSET_KEY, ASSET_VALUE, current_asset_ptr, dest_ptr]
117
118                padw padw swapdw
119                # => [ASSET_KEY, ASSET_VALUE, pad(12), dest_ptr]
120
121                call.wallet::receive_asset
122                # => [pad(16), dest_ptr]
123
124                dropw dropw dropw dropw
125                # => [dest_ptr]
126                ",
127        ));
128    }
129    code_body.push_str("dropw dropw dropw dropw");
130
131    let code = format!(
132        r#"
133        use mock::account
134        use miden::protocol::active_note
135        use ::miden::protocol::asset::ASSET_VALUE_MEMORY_OFFSET
136        use ::miden::protocol::asset::ASSET_SIZE
137        use miden::standards::wallets::basic->wallet
138
139        @note_script
140        pub proc main
141            # fetch pointer & number of assets
142            push.0 exec.active_note::get_assets     # [num_assets]
143
144            # runtime-check we got the expected count
145            push.{num_assets} assert_eq.err="unexpected number of assets"             # []
146
147            push.0                                                       # [dest_ptr]
148
149            {code_body}
150            dropw dropw dropw dropw
151        end
152        "#,
153        num_assets = assets.len(),
154    );
155
156    NoteBuilder::new(sender, SmallRng::from_seed([0; 32]))
157        .add_assets(assets.iter().copied())
158        .note_type(note_type)
159        .serial_number(serial_number)
160        .code(code)
161        .dynamically_linked_libraries(CodeBuilder::mock_libraries())
162        .build()
163        .expect("generated note script should compile")
164}
165
166/// Creates a `SPAWN` note.
167///
168///  A `SPAWN` note contains a note script that creates all `output_notes` that get passed as a
169///  parameter.
170///
171/// # Errors
172///
173/// Returns an error if:
174/// - the sender account ID of the provided output notes is not consistent or does not match the
175///   transaction's sender.
176pub fn create_spawn_note<'note, I>(
177    output_notes: impl IntoIterator<Item = &'note Note, IntoIter = I>,
178) -> anyhow::Result<Note>
179where
180    I: ExactSizeIterator<Item = &'note Note>,
181{
182    let mut output_notes = output_notes.into_iter().peekable();
183    if output_notes.len() == 0 {
184        anyhow::bail!("at least one output note is needed to create a SPAWN note");
185    }
186
187    let sender_id = output_notes
188        .peek()
189        .expect("at least one output note should be present")
190        .metadata()
191        .sender();
192
193    let (note_code, advice_map) = note_script_that_creates_notes(sender_id, output_notes)?;
194
195    let note = NoteBuilder::new(sender_id, SmallRng::from_os_rng())
196        .code(note_code)
197        .advice_map(advice_map)
198        .dynamically_linked_libraries(CodeBuilder::mock_libraries())
199        .build()?;
200
201    Ok(note)
202}
203
204/// Returns the code for a note that creates all notes in `output_notes`, along with an
205/// advice map containing the elements for any attachments keyed by their commitment.
206fn note_script_that_creates_notes<'note>(
207    sender_id: AccountId,
208    output_notes: impl Iterator<Item = &'note Note>,
209) -> anyhow::Result<(String, AdviceMap)> {
210    let mut advice_map = AdviceMap::default();
211    let mut out = String::from("use miden::protocol::output_note\n\n@note_script\npub proc main\n");
212
213    for (idx, note) in output_notes.into_iter().enumerate() {
214        anyhow::ensure!(
215            note.metadata().sender() == sender_id,
216            "sender IDs of output notes passed to SPAWN note are inconsistent"
217        );
218
219        // Make sure that the transaction's native account matches the note sender.
220        out.push_str(&format!(
221            r#"exec.::miden::protocol::native_account::get_id
222             # => [native_account_id_suffix, native_account_id_prefix]
223             push.{sender_suffix} assert_eq.err="sender ID suffix does not match native account ID's suffix"
224             # => [native_account_id_prefix]
225             push.{sender_prefix} assert_eq.err="sender ID prefix does not match native account ID's prefix"
226             # => []
227        "#,
228          sender_prefix = sender_id.prefix().as_felt(),
229          sender_suffix = sender_id.suffix()
230        ));
231
232        if idx == 0 {
233            out.push_str("padw padw\n");
234        } else {
235            out.push_str("dropw dropw dropw\n");
236        }
237        out.push_str(&format!(
238            "
239            push.{recipient}
240            push.{note_type}
241            push.{tag}
242            exec.output_note::create\n",
243            recipient = note.recipient().digest(),
244            note_type = note.metadata().note_type() as u8,
245            tag = note.metadata().tag(),
246        ));
247
248        for attachment in note.attachments().iter() {
249            let attachment_scheme = attachment.attachment_scheme().as_u16();
250            let commitment = attachment.content().to_commitment();
251
252            out.push_str(&format!(
253                "
254                      dup
255                      push.{commitment}
256                      push.{attachment_scheme}
257                      # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx, note_idx]
258                      exec.output_note::add_attachment
259                      # => [note_idx]
260                    ",
261            ));
262
263            // Add the elements to the advice map keyed by the commitment.
264            advice_map.insert(commitment, attachment.content().to_elements());
265        }
266
267        for asset in note.assets().iter() {
268            out.push_str(&format!(
269                " dup
270                  push.{ASSET_VALUE}
271                  push.{ASSET_KEY}
272                  # => [ASSET_KEY, ASSET_VALUE, note_idx, note_idx]
273                  call.::miden::standards::wallets::basic::move_asset_to_note
274                  # => [note_idx]
275                ",
276                ASSET_KEY = asset.to_key_word(),
277                ASSET_VALUE = asset.to_value_word(),
278            ));
279        }
280    }
281
282    out.push_str("repeat.5 dropw end\nend");
283
284    Ok((out, advice_map))
285}
286
287/// Generates a P2ID note - Pay-to-ID note with an exact serial number
288pub fn create_p2id_note_exact(
289    sender: AccountId,
290    target: AccountId,
291    assets: Vec<Asset>,
292    note_type: NoteType,
293    serial_num: Word,
294) -> Result<Note, NoteError> {
295    let recipient = P2idNoteStorage::new(target).into_recipient(serial_num);
296
297    let tag = NoteTag::with_account_target(target);
298
299    let metadata = PartialNoteMetadata::new(sender, note_type).with_tag(tag);
300    let vault = NoteAssets::new(assets)?;
301
302    Ok(Note::new(vault, metadata, recipient))
303}