miden_lib/transaction/
mod.rs

1use alloc::string::ToString;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4
5use miden_objects::account::AccountId;
6#[cfg(any(feature = "testing", test))]
7use miden_objects::assembly::Library;
8use miden_objects::assembly::debuginfo::SourceManagerSync;
9use miden_objects::assembly::{Assembler, DefaultSourceManager, KernelLibrary};
10use miden_objects::asset::FungibleAsset;
11use miden_objects::block::BlockNumber;
12use miden_objects::transaction::{
13    OutputNote,
14    OutputNotes,
15    TransactionArgs,
16    TransactionInputs,
17    TransactionOutputs,
18};
19use miden_objects::utils::serde::Deserializable;
20use miden_objects::utils::sync::LazyLock;
21use miden_objects::vm::{AdviceInputs, Program, ProgramInfo, StackInputs, StackOutputs};
22use miden_objects::{Felt, Hasher, TransactionOutputError, Word};
23use miden_stdlib::StdLibrary;
24
25use super::MidenLib;
26
27pub mod memory;
28
29mod events;
30pub use events::TransactionEvent;
31
32mod inputs;
33pub use inputs::{TransactionAdviceInputs, TransactionAdviceMapMismatch};
34
35mod outputs;
36pub use outputs::{
37    ACCOUNT_UPDATE_COMMITMENT_WORD_IDX,
38    EXPIRATION_BLOCK_ELEMENT_IDX,
39    FEE_ASSET_WORD_IDX,
40    OUTPUT_NOTES_COMMITMENT_WORD_IDX,
41    parse_final_account_header,
42};
43
44pub use crate::errors::{
45    TransactionEventError,
46    TransactionKernelError,
47    TransactionTraceParsingError,
48};
49
50mod procedures;
51
52// CONSTANTS
53// ================================================================================================
54
55// Initialize the kernel library only once
56static KERNEL_LIB: LazyLock<KernelLibrary> = LazyLock::new(|| {
57    let kernel_lib_bytes =
58        include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_kernel.masl"));
59    KernelLibrary::read_from_bytes(kernel_lib_bytes)
60        .expect("failed to deserialize transaction kernel library")
61});
62
63// Initialize the kernel main program only once
64static KERNEL_MAIN: LazyLock<Program> = LazyLock::new(|| {
65    let kernel_main_bytes =
66        include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_kernel.masb"));
67    Program::read_from_bytes(kernel_main_bytes)
68        .expect("failed to deserialize transaction kernel runtime")
69});
70
71// Initialize the transaction script executor program only once
72static TX_SCRIPT_MAIN: LazyLock<Program> = LazyLock::new(|| {
73    let tx_script_main_bytes =
74        include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_script_main.masb"));
75    Program::read_from_bytes(tx_script_main_bytes)
76        .expect("failed to deserialize tx script executor runtime")
77});
78
79// TRANSACTION KERNEL
80// ================================================================================================
81
82pub struct TransactionKernel;
83
84impl TransactionKernel {
85    // KERNEL SOURCE CODE
86    // --------------------------------------------------------------------------------------------
87
88    /// Returns a library with the transaction kernel system procedures.
89    ///
90    /// # Panics
91    /// Panics if the transaction kernel source is not well-formed.
92    pub fn kernel() -> KernelLibrary {
93        KERNEL_LIB.clone()
94    }
95
96    /// Returns an AST of the transaction kernel executable program.
97    ///
98    /// # Panics
99    /// Panics if the transaction kernel source is not well-formed.
100    pub fn main() -> Program {
101        KERNEL_MAIN.clone()
102    }
103
104    /// Returns an AST of the transaction script executor program.
105    ///
106    /// # Panics
107    /// Panics if the transaction kernel source is not well-formed.
108    pub fn tx_script_main() -> Program {
109        TX_SCRIPT_MAIN.clone()
110    }
111
112    /// Returns [ProgramInfo] for the transaction kernel executable program.
113    ///
114    /// # Panics
115    /// Panics if the transaction kernel source is not well-formed.
116    pub fn program_info() -> ProgramInfo {
117        // TODO: make static
118        let program_hash = Self::main().hash();
119        let kernel = Self::kernel().kernel().clone();
120
121        ProgramInfo::new(program_hash, kernel)
122    }
123
124    /// Transforms the provided [TransactionInputs] and [TransactionArgs] into stack and advice
125    /// inputs needed to execute a transaction kernel for a specific transaction.
126    ///
127    /// If `init_advice_inputs` is provided, they will be included in the returned advice inputs.
128    pub fn prepare_inputs(
129        tx_inputs: &TransactionInputs,
130        tx_args: &TransactionArgs,
131        init_advice_inputs: Option<AdviceInputs>,
132    ) -> Result<(StackInputs, TransactionAdviceInputs), TransactionAdviceMapMismatch> {
133        let account = tx_inputs.account();
134
135        let stack_inputs = TransactionKernel::build_input_stack(
136            account.id(),
137            account.init_commitment(),
138            tx_inputs.input_notes().commitment(),
139            tx_inputs.block_header().commitment(),
140            tx_inputs.block_header().block_num(),
141        );
142
143        let mut tx_advice_inputs = TransactionAdviceInputs::new(tx_inputs, tx_args)?;
144        if let Some(init_advice_inputs) = init_advice_inputs {
145            tx_advice_inputs.extend(init_advice_inputs);
146        }
147
148        Ok((stack_inputs, tx_advice_inputs))
149    }
150
151    // ASSEMBLER CONSTRUCTOR
152    // --------------------------------------------------------------------------------------------
153
154    /// Returns a new Miden assembler instantiated with the transaction kernel and loaded with the
155    /// Miden stdlib as well as with miden-lib.
156    pub fn assembler() -> Assembler {
157        Self::assembler_with_source_manager(Arc::new(DefaultSourceManager::default()))
158    }
159
160    /// Returns a new assembler instantiated with the transaction kernel and loaded with the
161    /// Miden stdlib as well as with miden-lib.
162    pub fn assembler_with_source_manager(source_manager: Arc<dyn SourceManagerSync>) -> Assembler {
163        #[cfg(all(any(feature = "testing", test), feature = "std"))]
164        source_manager_ext::load_masm_source_files(&source_manager);
165
166        Assembler::with_kernel(source_manager, Self::kernel())
167            .with_dynamic_library(StdLibrary::default())
168            .expect("failed to load std-lib")
169            .with_dynamic_library(MidenLib::default())
170            .expect("failed to load miden-lib")
171    }
172
173    // STACK INPUTS / OUTPUTS
174    // --------------------------------------------------------------------------------------------
175
176    /// Returns the stack with the public inputs required by the transaction kernel.
177    ///
178    /// The initial stack is defined:
179    ///
180    /// ```text
181    /// [
182    ///     BLOCK_COMMITMENT,
183    ///     INITIAL_ACCOUNT_COMMITMENT,
184    ///     INPUT_NOTES_COMMITMENT,
185    ///     account_id_prefix, account_id_suffix, block_num
186    /// ]
187    /// ```
188    ///
189    /// Where:
190    /// - BLOCK_COMMITMENT is the commitment to the reference block of the transaction.
191    /// - block_num is the reference block number.
192    /// - account_id_{prefix,suffix} are the prefix and suffix felts of the account that the
193    ///   transaction is being executed against.
194    /// - INITIAL_ACCOUNT_COMMITMENT is the account state prior to the transaction, EMPTY_WORD for
195    ///   new accounts.
196    /// - INPUT_NOTES_COMMITMENT, see `transaction::api::get_input_notes_commitment`.
197    pub fn build_input_stack(
198        account_id: AccountId,
199        init_account_commitment: Word,
200        input_notes_commitment: Word,
201        block_commitment: Word,
202        block_num: BlockNumber,
203    ) -> StackInputs {
204        // Note: Must be kept in sync with the transaction's kernel prepare_transaction procedure
205        let mut inputs: Vec<Felt> = Vec::with_capacity(14);
206        inputs.push(Felt::from(block_num));
207        inputs.push(account_id.suffix());
208        inputs.push(account_id.prefix().as_felt());
209        inputs.extend(input_notes_commitment);
210        inputs.extend_from_slice(init_account_commitment.as_elements());
211        inputs.extend_from_slice(block_commitment.as_elements());
212        StackInputs::new(inputs)
213            .map_err(|e| e.to_string())
214            .expect("Invalid stack input")
215    }
216
217    /// Builds the stack for expected transaction execution outputs.
218    /// The transaction kernel's output stack is formed like so:
219    ///
220    /// ```text
221    /// [
222    ///     OUTPUT_NOTES_COMMITMENT,
223    ///     ACCOUNT_UPDATE_COMMITMENT,
224    ///     FEE_ASSET,
225    ///     expiration_block_num,
226    /// ]
227    /// ```
228    ///
229    /// Where:
230    /// - OUTPUT_NOTES_COMMITMENT is a commitment to the output notes.
231    /// - ACCOUNT_UPDATE_COMMITMENT is the hash of the the final account commitment and account
232    ///   delta commitment.
233    /// - FEE_ASSET is the fungible asset used as the transaction fee.
234    /// - expiration_block_num is the block number at which the transaction will expire.
235    pub fn build_output_stack(
236        final_account_commitment: Word,
237        account_delta_commitment: Word,
238        output_notes_commitment: Word,
239        fee: FungibleAsset,
240        expiration_block_num: BlockNumber,
241    ) -> StackOutputs {
242        let account_update_commitment =
243            Hasher::merge(&[final_account_commitment, account_delta_commitment]);
244        let mut outputs: Vec<Felt> = Vec::with_capacity(9);
245        outputs.push(Felt::from(expiration_block_num));
246        outputs.extend(Word::from(fee));
247        outputs.extend(account_update_commitment);
248        outputs.extend(output_notes_commitment);
249        outputs.reverse();
250        StackOutputs::new(outputs)
251            .map_err(|e| e.to_string())
252            .expect("Invalid stack output")
253    }
254
255    /// Extracts transaction output data from the provided stack outputs.
256    ///
257    /// The data on the stack is expected to be arranged as follows:
258    ///
259    /// ```text
260    /// [
261    ///     OUTPUT_NOTES_COMMITMENT,
262    ///     ACCOUNT_UPDATE_COMMITMENT,
263    ///     FEE_ASSET,
264    ///     expiration_block_num,
265    /// ]
266    /// ```
267    ///
268    /// Where:
269    /// - OUTPUT_NOTES_COMMITMENT is the commitment of the output notes.
270    /// - ACCOUNT_UPDATE_COMMITMENT is the hash of the the final account commitment and account
271    ///   delta commitment.
272    /// - FEE_ASSET is the fungible asset used as the transaction fee.
273    /// - tx_expiration_block_num is the block height at which the transaction will become expired,
274    ///   defined by the sum of the execution block ref and the transaction's block expiration delta
275    ///   (if set during transaction execution).
276    ///
277    /// # Errors
278    ///
279    /// Returns an error if:
280    /// - Indices 13..16 on the stack are not zeroes.
281    /// - Overflow addresses are not empty.
282    pub fn parse_output_stack(
283        stack: &StackOutputs,
284    ) -> Result<(Word, Word, FungibleAsset, BlockNumber), TransactionOutputError> {
285        let output_notes_commitment = stack
286            .get_stack_word(OUTPUT_NOTES_COMMITMENT_WORD_IDX * 4)
287            .expect("output_notes_commitment (first word) missing");
288
289        let account_update_commitment = stack
290            .get_stack_word(ACCOUNT_UPDATE_COMMITMENT_WORD_IDX * 4)
291            .expect("account_update_commitment (second word) missing");
292
293        let fee = stack
294            .get_stack_word(FEE_ASSET_WORD_IDX * 4)
295            .expect("fee_asset (third word) missing");
296
297        let expiration_block_num = stack
298            .get_stack_item(EXPIRATION_BLOCK_ELEMENT_IDX)
299            .expect("tx_expiration_block_num (element on index 12) missing");
300
301        let expiration_block_num = u32::try_from(expiration_block_num.as_int())
302            .map_err(|_| {
303                TransactionOutputError::OutputStackInvalid(
304                    "expiration block number should be smaller than u32::MAX".into(),
305                )
306            })?
307            .into();
308
309        // Make sure that indices 13, 14 and 15 are zeroes (i.e. the fourth word without the
310        // expiration block number).
311        if stack.get_stack_word(12).expect("fourth word missing").as_elements()[..3]
312            != Word::empty().as_elements()[..3]
313        {
314            return Err(TransactionOutputError::OutputStackInvalid(
315                "indices 13, 14 and 15 on the output stack should be ZERO".into(),
316            ));
317        }
318
319        let fee = FungibleAsset::try_from(fee)
320            .map_err(TransactionOutputError::FeeAssetNotFungibleAsset)?;
321
322        Ok((output_notes_commitment, account_update_commitment, fee, expiration_block_num))
323    }
324
325    // TRANSACTION OUTPUT PARSER
326    // --------------------------------------------------------------------------------------------
327
328    /// Returns [TransactionOutputs] constructed from the provided output stack and advice map.
329    ///
330    /// The output stack is expected to be arranged as follows:
331    ///
332    /// ```text
333    /// [
334    ///     OUTPUT_NOTES_COMMITMENT,
335    ///     ACCOUNT_UPDATE_COMMITMENT,
336    ///     FEE_ASSET,
337    ///     expiration_block_num,
338    /// ]
339    /// ```
340    ///
341    /// Where:
342    /// - OUTPUT_NOTES_COMMITMENT is the commitment of the output notes.
343    /// - ACCOUNT_UPDATE_COMMITMENT is the hash of the final account commitment and the account
344    ///   delta commitment of the account that the transaction is being executed against.
345    /// - FEE_ASSET is the fungible asset used as the transaction fee.
346    /// - tx_expiration_block_num is the block height at which the transaction will become expired,
347    ///   defined by the sum of the execution block ref and the transaction's block expiration delta
348    ///   (if set during transaction execution).
349    ///
350    /// The actual data describing the new account state and output notes is expected to be located
351    /// in the provided advice map under keys `OUTPUT_NOTES_COMMITMENT` and
352    /// `ACCOUNT_UPDATE_COMMITMENT`, where the final data for the account state is located under
353    /// `FINAL_ACCOUNT_COMMITMENT`.
354    pub fn from_transaction_parts(
355        stack: &StackOutputs,
356        advice_inputs: &AdviceInputs,
357        output_notes: Vec<OutputNote>,
358    ) -> Result<TransactionOutputs, TransactionOutputError> {
359        let (output_notes_commitment, account_update_commitment, fee, expiration_block_num) =
360            Self::parse_output_stack(stack)?;
361
362        let (final_account_commitment, account_delta_commitment) =
363            Self::parse_account_update_commitment(account_update_commitment, advice_inputs)?;
364
365        // parse final account state
366        let final_account_data = advice_inputs
367            .map
368            .get(&final_account_commitment)
369            .ok_or(TransactionOutputError::FinalAccountCommitmentMissingInAdviceMap)?;
370
371        let account = parse_final_account_header(final_account_data)
372            .map_err(TransactionOutputError::FinalAccountHeaderParseFailure)?;
373
374        // validate output notes
375        let output_notes = OutputNotes::new(output_notes)?;
376        if output_notes_commitment != output_notes.commitment() {
377            return Err(TransactionOutputError::OutputNotesCommitmentInconsistent {
378                actual: output_notes.commitment(),
379                expected: output_notes_commitment,
380            });
381        }
382
383        Ok(TransactionOutputs {
384            account,
385            account_delta_commitment,
386            output_notes,
387            fee,
388            expiration_block_num,
389        })
390    }
391
392    /// Returns the final account commitment and account delta commitment extracted from the account
393    /// update commitment.
394    fn parse_account_update_commitment(
395        account_update_commitment: Word,
396        advice_inputs: &AdviceInputs,
397    ) -> Result<(Word, Word), TransactionOutputError> {
398        let account_update_data =
399            advice_inputs.map.get(&account_update_commitment).ok_or_else(|| {
400                TransactionOutputError::AccountUpdateCommitment(
401                    "failed to find ACCOUNT_UPDATE_COMMITMENT in advice map".into(),
402                )
403            })?;
404
405        if account_update_data.len() != 8 {
406            return Err(TransactionOutputError::AccountUpdateCommitment(
407                "expected account update commitment advice map entry to contain exactly 8 elements"
408                    .into(),
409            ));
410        }
411
412        // SAFETY: We just asserted that the data is of length 8 so slicing the data into two words
413        // is fine.
414        let final_account_commitment = Word::from(
415            <[Felt; 4]>::try_from(&account_update_data[0..4])
416                .expect("we should have sliced off exactly four elements"),
417        );
418        let account_delta_commitment = Word::from(
419            <[Felt; 4]>::try_from(&account_update_data[4..8])
420                .expect("we should have sliced off exactly four elements"),
421        );
422
423        let computed_account_update_commitment =
424            Hasher::merge(&[final_account_commitment, account_delta_commitment]);
425
426        if computed_account_update_commitment != account_update_commitment {
427            let err_message = format!(
428                "transaction outputs account update commitment {account_update_commitment} but commitment computed from its advice map entries was {computed_account_update_commitment}"
429            );
430            return Err(TransactionOutputError::AccountUpdateCommitment(err_message.into()));
431        }
432
433        Ok((final_account_commitment, account_delta_commitment))
434    }
435}
436
437#[cfg(any(feature = "testing", test))]
438impl TransactionKernel {
439    const KERNEL_TESTING_LIB_BYTES: &'static [u8] =
440        include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/kernel_library.masl"));
441
442    /// Returns the kernel library.
443    pub fn library() -> Library {
444        Library::read_from_bytes(Self::KERNEL_TESTING_LIB_BYTES)
445            .expect("failed to deserialize transaction kernel library")
446    }
447
448    /// Returns the mock account and faucet libraries.
449    pub fn mock_libraries() -> impl Iterator<Item = Library> {
450        use miden_objects::account::AccountCode;
451
452        use crate::testing::mock_account_code::MockAccountCodeExt;
453
454        vec![AccountCode::mock_account_library(), AccountCode::mock_faucet_library()].into_iter()
455    }
456
457    /// Returns an [`Assembler`] with the transaction kernel as a library.
458    ///
459    /// This assembler is the same as [`TransactionKernel::assembler`] but additionally includes the
460    /// kernel library on the namespace of `$kernel`. The `$kernel` library is added separately
461    /// because even though the library (`api.masm`) and the kernel binary (`main.masm`) include
462    /// this code, it is not otherwise accessible. By adding it separately, we can invoke procedures
463    /// from the kernel library to test them individually.
464    pub fn with_kernel_library(source_manager: Arc<dyn SourceManagerSync>) -> Assembler {
465        Self::assembler_with_source_manager(source_manager)
466            .with_dynamic_library(Self::library())
467            .expect("failed to load kernel library (/lib)")
468            .with_debug_mode(true)
469    }
470
471    /// Returns an [`Assembler`] with the mock account and faucet libraries.
472    ///
473    /// This assembler is the same as [`TransactionKernel::with_kernel_library`] but additionally
474    /// includes the [`MockAccountCodeExt::mock_account_library`][account_lib]
475    /// and [`MockAccountCodeExt::mock_faucet_library`][faucet_lib], which are the standard
476    /// testing account libraries.
477    ///
478    /// [account_lib]: crate::testing::mock_account_code::MockAccountCodeExt::mock_account_library
479    /// [faucet_lib]: crate::testing::mock_account_code::MockAccountCodeExt::mock_faucet_library
480    pub fn with_mock_libraries(source_manager: Arc<dyn SourceManagerSync>) -> Assembler {
481        let mut assembler = Self::with_kernel_library(source_manager);
482
483        for library in Self::mock_libraries() {
484            assembler
485                .link_dynamic_library(library)
486                .expect("failed to add mock account libraries");
487        }
488
489        assembler
490    }
491}
492
493#[cfg(all(any(feature = "testing", test), feature = "std"))]
494mod source_manager_ext {
495    use std::path::{Path, PathBuf};
496    use std::vec::Vec;
497    use std::{fs, io};
498
499    use miden_objects::assembly::SourceManager;
500    use miden_objects::assembly::debuginfo::SourceManagerExt;
501
502    /// Loads all files with a .masm extension in the `asm` directory into the provided source
503    /// manager.
504    ///
505    /// This source manager is passed to the [`super::TransactionKernel::assembler`] from which it
506    /// can be passed on to the VM processor. If an error occurs, the sources can be used to provide
507    /// a pointer to the failed location.
508    pub fn load_masm_source_files(source_manager: &dyn SourceManager) {
509        if let Err(err) = load(source_manager) {
510            // Stringifying the error is not ideal (we may loose some source errors) but this
511            // should never really error anyway.
512            std::eprintln!("failed to load MASM sources into source manager: {err}");
513        }
514    }
515
516    /// Implements the logic of the above function with error handling.
517    fn load(source_manager: &dyn SourceManager) -> io::Result<()> {
518        for file in get_masm_files(concat!(env!("OUT_DIR"), "/asm"))? {
519            source_manager.load_file(&file).map_err(io::Error::other)?;
520        }
521
522        Ok(())
523    }
524
525    /// Returns a vector with paths to all MASM files in the specified directory and recursive
526    /// directories.
527    ///
528    /// All non-MASM files are skipped.
529    fn get_masm_files<P: AsRef<Path>>(dir_path: P) -> io::Result<Vec<PathBuf>> {
530        let mut files = Vec::new();
531
532        match fs::read_dir(dir_path) {
533            Ok(entries) => {
534                for entry in entries {
535                    match entry {
536                        Ok(entry) => {
537                            let entry_path = entry.path();
538                            if entry_path.is_dir() {
539                                files.extend(get_masm_files(entry_path)?);
540                            } else if entry_path
541                                .extension()
542                                .map(|ext| ext == "masm")
543                                .unwrap_or(false)
544                            {
545                                files.push(entry_path);
546                            }
547                        },
548                        Err(e) => {
549                            return Err(io::Error::other(format!(
550                                "error reading directory entry: {e}",
551                            )));
552                        },
553                    }
554                }
555            },
556            Err(e) => {
557                return Err(io::Error::other(format!("error reading directory: {e}")));
558            },
559        }
560
561        Ok(files)
562    }
563}