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