Skip to main content

miden_protocol/transaction/kernel/
mod.rs

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