miden_lib/transaction/
mod.rs

1use alloc::{string::ToString, sync::Arc, vec::Vec};
2
3use miden_objects::{
4    Digest, EMPTY_WORD, Felt, TransactionOutputError, ZERO,
5    account::{AccountCode, AccountHeader, AccountId, AccountStorageHeader},
6    assembly::{Assembler, DefaultSourceManager, KernelLibrary},
7    block::BlockNumber,
8    crypto::merkle::{MerkleError, MerklePath},
9    transaction::{
10        OutputNote, OutputNotes, TransactionArgs, TransactionInputs, TransactionOutputs,
11    },
12    utils::{serde::Deserializable, sync::LazyLock},
13    vm::{AdviceInputs, AdviceMap, Program, ProgramInfo, StackInputs, StackOutputs},
14};
15use miden_stdlib::StdLibrary;
16use outputs::EXPIRATION_BLOCK_ELEMENT_IDX;
17
18use super::MidenLib;
19
20pub mod memory;
21
22mod events;
23pub use events::{TransactionEvent, TransactionTrace};
24
25mod inputs;
26
27mod outputs;
28pub use outputs::{
29    FINAL_ACCOUNT_COMMITMENT_WORD_IDX, OUTPUT_NOTES_COMMITMENT_WORD_IDX, parse_final_account_header,
30};
31
32mod errors;
33pub use errors::{TransactionEventError, TransactionKernelError, TransactionTraceParsingError};
34
35mod procedures;
36
37// CONSTANTS
38// ================================================================================================
39
40// Initialize the kernel library only once
41static KERNEL_LIB: LazyLock<KernelLibrary> = LazyLock::new(|| {
42    let kernel_lib_bytes =
43        include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_kernel.masl"));
44    KernelLibrary::read_from_bytes(kernel_lib_bytes)
45        .expect("failed to deserialize transaction kernel library")
46});
47
48// Initialize the kernel main program only once
49static KERNEL_MAIN: LazyLock<Program> = LazyLock::new(|| {
50    let kernel_main_bytes =
51        include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_kernel.masb"));
52    Program::read_from_bytes(kernel_main_bytes)
53        .expect("failed to deserialize transaction kernel runtime")
54});
55
56// Initialize the transaction script executor program only once
57static TX_SCRIPT_MAIN: LazyLock<Program> = LazyLock::new(|| {
58    let tx_script_main_bytes =
59        include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_script_main.masb"));
60    Program::read_from_bytes(tx_script_main_bytes)
61        .expect("failed to deserialize tx script executor runtime")
62});
63
64// TRANSACTION KERNEL
65// ================================================================================================
66
67pub struct TransactionKernel;
68
69impl TransactionKernel {
70    // KERNEL SOURCE CODE
71    // --------------------------------------------------------------------------------------------
72
73    /// Returns a library with the transaction kernel system procedures.
74    ///
75    /// # Panics
76    /// Panics if the transaction kernel source is not well-formed.
77    pub fn kernel() -> KernelLibrary {
78        KERNEL_LIB.clone()
79    }
80
81    /// Returns an AST of the transaction kernel executable program.
82    ///
83    /// # Panics
84    /// Panics if the transaction kernel source is not well-formed.
85    pub fn main() -> Program {
86        KERNEL_MAIN.clone()
87    }
88
89    /// Returns an AST of the transaction script executor program.
90    ///
91    /// # Panics
92    /// Panics if the transaction kernel source is not well-formed.
93    pub fn tx_script_main() -> Program {
94        TX_SCRIPT_MAIN.clone()
95    }
96
97    /// Returns [ProgramInfo] for the transaction kernel executable program.
98    ///
99    /// # Panics
100    /// Panics if the transaction kernel source is not well-formed.
101    pub fn program_info() -> ProgramInfo {
102        // TODO: make static
103        let program_hash = Self::main().hash();
104        let kernel = Self::kernel().kernel().clone();
105
106        ProgramInfo::new(program_hash, kernel)
107    }
108
109    /// Transforms the provided [TransactionInputs] and [TransactionArgs] into stack and advice
110    /// inputs needed to execute a transaction kernel for a specific transaction.
111    ///
112    /// If `init_advice_inputs` is provided, they will be included in the returned advice inputs.
113    pub fn prepare_inputs(
114        tx_inputs: &TransactionInputs,
115        tx_args: &TransactionArgs,
116        init_advice_inputs: Option<AdviceInputs>,
117    ) -> (StackInputs, AdviceInputs) {
118        let account = tx_inputs.account();
119
120        let stack_inputs = TransactionKernel::build_input_stack(
121            account.id(),
122            account.init_commitment(),
123            tx_inputs.input_notes().commitment(),
124            tx_inputs.block_header().commitment(),
125            tx_inputs.block_header().block_num(),
126        );
127
128        let mut advice_inputs = init_advice_inputs.unwrap_or_default();
129        inputs::extend_advice_inputs(tx_inputs, tx_args, &mut advice_inputs);
130
131        (stack_inputs, advice_inputs)
132    }
133
134    // ASSEMBLER CONSTRUCTOR
135    // --------------------------------------------------------------------------------------------
136
137    /// Returns a new Miden assembler instantiated with the transaction kernel and loaded with the
138    /// Miden stdlib as well as with miden-lib.
139    pub fn assembler() -> Assembler {
140        let source_manager = Arc::new(DefaultSourceManager::default());
141        Assembler::with_kernel(source_manager, Self::kernel())
142            .with_library(StdLibrary::default())
143            .expect("failed to load std-lib")
144            .with_library(MidenLib::default())
145            .expect("failed to load miden-lib")
146    }
147
148    // STACK INPUTS / OUTPUTS
149    // --------------------------------------------------------------------------------------------
150
151    /// Returns the stack with the public inputs required by the transaction kernel.
152    ///
153    /// The initial stack is defined:
154    ///
155    /// ```text
156    /// [
157    ///     BLOCK_COMMITMENT,
158    ///     INITIAL_ACCOUNT_COMMITMENT,
159    ///     INPUT_NOTES_COMMITMENT,
160    ///     account_id_prefix, account_id_suffix, block_num
161    /// ]
162    /// ```
163    ///
164    /// Where:
165    /// - BLOCK_COMMITMENT is the commitment to the reference block of the transaction.
166    /// - block_num is the reference block number.
167    /// - account_id_{prefix,suffix} are the prefix and suffix felts of the account that the
168    ///   transaction is being executed against.
169    /// - INITIAL_ACCOUNT_COMMITMENT is the account state prior to the transaction, EMPTY_WORD for
170    ///   new accounts.
171    /// - INPUT_NOTES_COMMITMENT, see `transaction::api::get_input_notes_commitment`.
172    pub fn build_input_stack(
173        account_id: AccountId,
174        init_account_commitment: Digest,
175        input_notes_commitment: Digest,
176        block_commitment: Digest,
177        block_num: BlockNumber,
178    ) -> StackInputs {
179        // Note: Must be kept in sync with the transaction's kernel prepare_transaction procedure
180        let mut inputs: Vec<Felt> = Vec::with_capacity(14);
181        inputs.push(Felt::from(block_num));
182        inputs.push(account_id.suffix());
183        inputs.push(account_id.prefix().as_felt());
184        inputs.extend(input_notes_commitment);
185        inputs.extend_from_slice(init_account_commitment.as_elements());
186        inputs.extend_from_slice(block_commitment.as_elements());
187        StackInputs::new(inputs)
188            .map_err(|e| e.to_string())
189            .expect("Invalid stack input")
190    }
191
192    /// Extends the advice inputs with account data and Merkle proofs.
193    ///
194    /// Where:
195    /// - account_header is the header of the account which data will be used for the extension.
196    /// - account_code is the code of the account which will be used for the extension.
197    /// - storage_header is the header of the storage which data will be used for the extension.
198    /// - merkle_path is the authentication path from the account root of the block header to the
199    ///   account.
200    pub fn extend_advice_inputs_for_account(
201        advice_inputs: &mut AdviceInputs,
202        account_header: &AccountHeader,
203        account_code: &AccountCode,
204        storage_header: &AccountStorageHeader,
205        merkle_path: &MerklePath,
206    ) -> Result<(), MerkleError> {
207        let account_id = account_header.id();
208        let storage_root = account_header.storage_commitment();
209        let code_root = account_header.code_commitment();
210        // Note: keep in sync with the start_foreign_context kernel procedure
211        let account_key =
212            Digest::from([account_id.suffix(), account_id.prefix().as_felt(), ZERO, ZERO]);
213
214        // Extend the advice inputs with the new data
215        advice_inputs.extend_map([
216            // ACCOUNT_ID -> [ID_AND_NONCE, VAULT_ROOT, STORAGE_ROOT, CODE_ROOT]
217            (account_key, account_header.as_elements()),
218            // STORAGE_ROOT -> [STORAGE_SLOT_DATA]
219            (storage_root, storage_header.as_elements()),
220            // CODE_ROOT -> [ACCOUNT_CODE_DATA]
221            (code_root, account_code.as_elements()),
222        ]);
223
224        // Extend the advice inputs with Merkle store data
225        advice_inputs.extend_merkle_store(
226            // The prefix is the index in the account tree.
227            merkle_path.inner_nodes(account_id.prefix().as_u64(), account_header.commitment())?,
228        );
229
230        Ok(())
231    }
232
233    /// Builds the stack for expected transaction execution outputs.
234    /// The transaction kernel's output stack is formed like so:
235    ///
236    /// ```text
237    /// [
238    ///     expiration_block_num,
239    ///     OUTPUT_NOTES_COMMITMENT,
240    ///     FINAL_ACCOUNT_COMMITMENT,
241    /// ]
242    /// ```
243    ///
244    /// Where:
245    /// - OUTPUT_NOTES_COMMITMENT is a commitment to the output notes.
246    /// - FINAL_ACCOUNT_COMMITMENT is a hash of the account's final state.
247    /// - expiration_block_num is the block number at which the transaction will expire.
248    pub fn build_output_stack(
249        final_account_commitment: Digest,
250        output_notes_commitment: Digest,
251        expiration_block_num: BlockNumber,
252    ) -> StackOutputs {
253        let mut outputs: Vec<Felt> = Vec::with_capacity(9);
254        outputs.push(Felt::from(expiration_block_num));
255        outputs.extend(final_account_commitment);
256        outputs.extend(output_notes_commitment);
257        outputs.reverse();
258        StackOutputs::new(outputs)
259            .map_err(|e| e.to_string())
260            .expect("Invalid stack output")
261    }
262
263    /// Extracts transaction output data from the provided stack outputs.
264    ///
265    /// The data on the stack is expected to be arranged as follows:
266    ///
267    /// Stack: [OUTPUT_NOTES_COMMITMENT, FINAL_ACCOUNT_COMMITMENT, tx_expiration_block_num]
268    ///
269    /// Where:
270    /// - OUTPUT_NOTES_COMMITMENT is the commitment of the output notes.
271    /// - FINAL_ACCOUNT_COMMITMENT is the final account commitment of the account that the
272    ///   transaction is being executed against.
273    /// - tx_expiration_block_num is the block height at which the transaction will become expired,
274    ///   defined by the sum of the execution block ref and the transaction's block expiration delta
275    ///   (if set during transaction execution).
276    ///
277    /// # Errors
278    /// Returns an error if:
279    /// - Words 3 and 4 on the stack are not 0.
280    /// - Overflow addresses are not empty.
281    pub fn parse_output_stack(
282        stack: &StackOutputs,
283    ) -> Result<(Digest, Digest, BlockNumber), TransactionOutputError> {
284        let output_notes_commitment = stack
285            .get_stack_word(OUTPUT_NOTES_COMMITMENT_WORD_IDX * 4)
286            .expect("first word missing")
287            .into();
288
289        let final_account_commitment = stack
290            .get_stack_word(FINAL_ACCOUNT_COMMITMENT_WORD_IDX * 4)
291            .expect("second word missing")
292            .into();
293
294        let expiration_block_num = stack
295            .get_stack_item(EXPIRATION_BLOCK_ELEMENT_IDX)
296            .expect("element on index 8 missing");
297
298        let expiration_block_num = u32::try_from(expiration_block_num.as_int())
299            .map_err(|_| {
300                TransactionOutputError::OutputStackInvalid(
301                    "Expiration block number should be smaller than u32::MAX".into(),
302                )
303            })?
304            .into();
305
306        if stack.get_stack_word(12).expect("fourth word missing") != EMPTY_WORD {
307            return Err(TransactionOutputError::OutputStackInvalid(
308                "Fourth word on output stack should consist only of ZEROs".into(),
309            ));
310        }
311
312        Ok((final_account_commitment, output_notes_commitment, expiration_block_num))
313    }
314
315    // TRANSACTION OUTPUT PARSER
316    // --------------------------------------------------------------------------------------------
317
318    /// Returns [TransactionOutputs] constructed from the provided output stack and advice map.
319    ///
320    /// The output stack is expected to be arrange as follows:
321    ///
322    /// Stack: [OUTPUT_NOTES_COMMITMENT, FINAL_ACCOUNT_COMMITMENT, tx_expiration_block_num]
323    ///
324    /// Where:
325    /// - OUTPUT_NOTES_COMMITMENT is the commitment of the output notes.
326    /// - FINAL_ACCOUNT_COMMITMENT is the final account commitment of the account that the
327    ///   transaction is being executed against.
328    /// - tx_expiration_block_num is the block height at which the transaction will become expired,
329    ///   defined by the sum of the execution block ref and the transaction's block expiration delta
330    ///   (if set during transaction execution).
331    ///
332    /// The actual data describing the new account state and output notes is expected to be located
333    /// in the provided advice map under keys `OUTPUT_NOTES_COMMITMENT` and
334    /// `FINAL_ACCOUNT_COMMITMENT`.
335    pub fn from_transaction_parts(
336        stack: &StackOutputs,
337        adv_map: &AdviceMap,
338        output_notes: Vec<OutputNote>,
339    ) -> Result<TransactionOutputs, TransactionOutputError> {
340        let (final_account_commitment, output_notes_commitment, expiration_block_num) =
341            Self::parse_output_stack(stack)?;
342
343        // parse final account state
344        let final_account_data = adv_map
345            .get(&final_account_commitment)
346            .ok_or(TransactionOutputError::FinalAccountHashMissingInAdviceMap)?;
347        let account = parse_final_account_header(final_account_data)
348            .map_err(TransactionOutputError::FinalAccountHeaderParseFailure)?;
349
350        // validate output notes
351        let output_notes = OutputNotes::new(output_notes)?;
352        if output_notes_commitment != output_notes.commitment() {
353            return Err(TransactionOutputError::OutputNotesCommitmentInconsistent {
354                actual: output_notes.commitment(),
355                expected: output_notes_commitment,
356            });
357        }
358
359        Ok(TransactionOutputs {
360            account,
361            output_notes,
362            expiration_block_num,
363        })
364    }
365}
366
367#[cfg(any(feature = "testing", test))]
368impl TransactionKernel {
369    const KERNEL_TESTING_LIB_BYTES: &'static [u8] =
370        include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/kernel_library.masl"));
371
372    pub fn kernel_as_library() -> miden_objects::assembly::Library {
373        miden_objects::assembly::Library::read_from_bytes(Self::KERNEL_TESTING_LIB_BYTES)
374            .expect("failed to deserialize transaction kernel library")
375    }
376
377    /// Contains code to get an instance of the [Assembler] that should be used in tests.
378    ///
379    /// This assembler is similar to the assembler used to assemble the kernel and transactions,
380    /// with the difference that it also includes an extra library on the namespace of `kernel`.
381    /// The `kernel` library is added separately because even though the library (`api.masm`) and
382    /// the kernel binary (`main.masm`) include this code, it is not exposed explicitly. By adding
383    /// it separately, we can expose procedures from `/lib` and test them individually.
384    pub fn testing_assembler() -> Assembler {
385        let source_manager = Arc::new(DefaultSourceManager::default());
386        let kernel_library = Self::kernel_as_library();
387
388        Assembler::with_kernel(source_manager, Self::kernel())
389            .with_library(StdLibrary::default())
390            .expect("failed to load std-lib")
391            .with_library(MidenLib::default())
392            .expect("failed to load miden-lib")
393            .with_library(kernel_library)
394            .expect("failed to load kernel library (/lib)")
395    }
396
397    /// Returns the testing assembler, and additionally contains the library for
398    /// [AccountCode::mock_library()], which is a mock wallet used in tests.
399    pub fn testing_assembler_with_mock_account() -> Assembler {
400        let assembler = Self::testing_assembler();
401        let library = AccountCode::mock_library(assembler.clone());
402
403        assembler.with_library(library).expect("failed to add mock account code")
404    }
405}