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}