Skip to main content

miden_protocol/transaction/kernel/
mod.rs

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