miden_lib/transaction/
mod.rs

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