miden_lib/transaction/
mod.rs

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