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