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