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