1use alloc::{string::ToString, sync::Arc, vec::Vec};
2
3use miden_objects::{
4 Digest, EMPTY_WORD, Felt, Hasher, TransactionOutputError,
5 account::AccountId,
6 assembly::{Assembler, DefaultSourceManager, KernelLibrary, SourceManager},
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;
25pub use inputs::TransactionAdviceInputs;
26
27mod outputs;
28pub use outputs::{
29 ACCOUNT_UPDATE_COMMITMENT_WORD_IDX, OUTPUT_NOTES_COMMITMENT_WORD_IDX,
30 parse_final_account_header,
31};
32
33pub use crate::errors::{
34 TransactionEventError, TransactionKernelError, TransactionTraceParsingError,
35};
36
37mod procedures;
38
39static KERNEL_LIB: LazyLock<KernelLibrary> = LazyLock::new(|| {
44 let kernel_lib_bytes =
45 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_kernel.masl"));
46 KernelLibrary::read_from_bytes(kernel_lib_bytes)
47 .expect("failed to deserialize transaction kernel library")
48});
49
50static KERNEL_MAIN: LazyLock<Program> = LazyLock::new(|| {
52 let kernel_main_bytes =
53 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_kernel.masb"));
54 Program::read_from_bytes(kernel_main_bytes)
55 .expect("failed to deserialize transaction kernel runtime")
56});
57
58static TX_SCRIPT_MAIN: LazyLock<Program> = LazyLock::new(|| {
60 let tx_script_main_bytes =
61 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_script_main.masb"));
62 Program::read_from_bytes(tx_script_main_bytes)
63 .expect("failed to deserialize tx script executor runtime")
64});
65
66pub struct TransactionKernel;
70
71impl TransactionKernel {
72 pub fn kernel() -> KernelLibrary {
80 KERNEL_LIB.clone()
81 }
82
83 pub fn main() -> Program {
88 KERNEL_MAIN.clone()
89 }
90
91 pub fn tx_script_main() -> Program {
96 TX_SCRIPT_MAIN.clone()
97 }
98
99 pub fn program_info() -> ProgramInfo {
104 let program_hash = Self::main().hash();
106 let kernel = Self::kernel().kernel().clone();
107
108 ProgramInfo::new(program_hash, kernel)
109 }
110
111 pub fn prepare_inputs(
116 tx_inputs: &TransactionInputs,
117 tx_args: &TransactionArgs,
118 init_advice_inputs: Option<AdviceInputs>,
119 ) -> (StackInputs, TransactionAdviceInputs) {
120 let account = tx_inputs.account();
121
122 let stack_inputs = TransactionKernel::build_input_stack(
123 account.id(),
124 account.init_commitment(),
125 tx_inputs.input_notes().commitment(),
126 tx_inputs.block_header().commitment(),
127 tx_inputs.block_header().block_num(),
128 );
129
130 let mut tx_advice_inputs = TransactionAdviceInputs::new(tx_inputs, tx_args);
131 if let Some(init_advice_inputs) = init_advice_inputs {
132 tx_advice_inputs.extend(init_advice_inputs);
133 }
134
135 (stack_inputs, tx_advice_inputs)
136 }
137
138 pub fn assembler() -> Assembler {
144 let source_manager: Arc<dyn SourceManager + Send + Sync> =
145 Arc::new(DefaultSourceManager::default());
146
147 #[cfg(all(any(feature = "testing", test), feature = "std"))]
148 source_manager_ext::load_masm_source_files(&source_manager);
149
150 Assembler::with_kernel(source_manager, Self::kernel())
151 .with_library(StdLibrary::default())
152 .expect("failed to load std-lib")
153 .with_library(MidenLib::default())
154 .expect("failed to load miden-lib")
155 }
156
157 pub fn build_input_stack(
182 account_id: AccountId,
183 init_account_commitment: Digest,
184 input_notes_commitment: Digest,
185 block_commitment: Digest,
186 block_num: BlockNumber,
187 ) -> StackInputs {
188 let mut inputs: Vec<Felt> = Vec::with_capacity(14);
190 inputs.push(Felt::from(block_num));
191 inputs.push(account_id.suffix());
192 inputs.push(account_id.prefix().as_felt());
193 inputs.extend(input_notes_commitment);
194 inputs.extend_from_slice(init_account_commitment.as_elements());
195 inputs.extend_from_slice(block_commitment.as_elements());
196 StackInputs::new(inputs)
197 .map_err(|e| e.to_string())
198 .expect("Invalid stack input")
199 }
200
201 pub fn build_output_stack(
218 final_account_commitment: Digest,
219 account_delta_commitment: Digest,
220 output_notes_commitment: Digest,
221 expiration_block_num: BlockNumber,
222 ) -> StackOutputs {
223 let account_update_commitment =
224 Hasher::merge(&[final_account_commitment, account_delta_commitment]);
225 let mut outputs: Vec<Felt> = Vec::with_capacity(9);
226 outputs.push(Felt::from(expiration_block_num));
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 pub fn parse_output_stack(
255 stack: &StackOutputs,
256 ) -> Result<(Digest, Digest, BlockNumber), TransactionOutputError> {
257 let output_notes_commitment = stack
258 .get_stack_word(OUTPUT_NOTES_COMMITMENT_WORD_IDX * 4)
259 .expect("output_notes_commitment (first word) missing")
260 .into();
261
262 let account_update_commitment = stack
263 .get_stack_word(ACCOUNT_UPDATE_COMMITMENT_WORD_IDX * 4)
264 .expect("account_update_commitment (second word) missing")
265 .into();
266
267 let expiration_block_num = stack
268 .get_stack_item(EXPIRATION_BLOCK_ELEMENT_IDX)
269 .expect("element on index 8 missing");
270
271 let expiration_block_num = u32::try_from(expiration_block_num.as_int())
272 .map_err(|_| {
273 TransactionOutputError::OutputStackInvalid(
274 "expiration block number should be smaller than u32::MAX".into(),
275 )
276 })?
277 .into();
278
279 if stack.get_stack_word(9).expect("third word missing")[..3] != EMPTY_WORD[..3] {
282 return Err(TransactionOutputError::OutputStackInvalid(
283 "indices 9, 10 and 11 on the output stack should be ZERO".into(),
284 ));
285 }
286
287 if stack.get_stack_word(12).expect("fourth word missing") != EMPTY_WORD {
288 return Err(TransactionOutputError::OutputStackInvalid(
289 "fourth word on output stack should consist only of ZEROs".into(),
290 ));
291 }
292
293 Ok((output_notes_commitment, account_update_commitment, expiration_block_num))
294 }
295
296 pub fn from_transaction_parts(
318 stack: &StackOutputs,
319 adv_map: &AdviceMap,
320 output_notes: Vec<OutputNote>,
321 ) -> Result<TransactionOutputs, TransactionOutputError> {
322 let (output_notes_commitment, account_update_commitment, expiration_block_num) =
323 Self::parse_output_stack(stack)?;
324
325 let (final_account_commitment, account_delta_commitment) =
326 Self::parse_account_update_commitment(account_update_commitment, adv_map)?;
327
328 let final_account_data = adv_map
330 .get(&final_account_commitment)
331 .ok_or(TransactionOutputError::FinalAccountCommitmentMissingInAdviceMap)?;
332
333 let account = parse_final_account_header(final_account_data)
334 .map_err(TransactionOutputError::FinalAccountHeaderParseFailure)?;
335
336 let output_notes = OutputNotes::new(output_notes)?;
338 if output_notes_commitment != output_notes.commitment() {
339 return Err(TransactionOutputError::OutputNotesCommitmentInconsistent {
340 actual: output_notes.commitment(),
341 expected: output_notes_commitment,
342 });
343 }
344
345 Ok(TransactionOutputs {
346 account,
347 account_delta_commitment,
348 output_notes,
349 expiration_block_num,
350 })
351 }
352
353 fn parse_account_update_commitment(
356 account_update_commitment: Digest,
357 adv_map: &AdviceMap,
358 ) -> Result<(Digest, Digest), TransactionOutputError> {
359 let account_update_data = adv_map.get(&account_update_commitment).ok_or_else(|| {
360 TransactionOutputError::AccountUpdateCommitment(
361 "failed to find ACCOUNT_UPDATE_COMMITMENT in advice map".into(),
362 )
363 })?;
364
365 if account_update_data.len() != 8 {
366 return Err(TransactionOutputError::AccountUpdateCommitment(
367 "expected account update commitment advice map entry to contain exactly 8 elements"
368 .into(),
369 ));
370 }
371
372 let final_account_commitment = Digest::from(
375 <[Felt; 4]>::try_from(&account_update_data[0..4])
376 .expect("we should have sliced off exactly four elements"),
377 );
378 let account_delta_commitment = Digest::from(
379 <[Felt; 4]>::try_from(&account_update_data[4..8])
380 .expect("we should have sliced off exactly four elements"),
381 );
382
383 let computed_account_update_commitment =
384 Hasher::merge(&[final_account_commitment, account_delta_commitment]);
385
386 if computed_account_update_commitment != account_update_commitment {
387 let err_message = format!(
388 "transaction outputs account update commitment {account_update_commitment} but commitment computed from its advice map entries was {computed_account_update_commitment}"
389 );
390 return Err(TransactionOutputError::AccountUpdateCommitment(err_message.into()));
391 }
392
393 Ok((final_account_commitment, account_delta_commitment))
394 }
395}
396
397#[cfg(any(feature = "testing", test))]
398impl TransactionKernel {
399 const KERNEL_TESTING_LIB_BYTES: &'static [u8] =
400 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/kernel_library.masl"));
401
402 pub fn kernel_as_library() -> miden_objects::assembly::Library {
403 miden_objects::assembly::Library::read_from_bytes(Self::KERNEL_TESTING_LIB_BYTES)
404 .expect("failed to deserialize transaction kernel library")
405 }
406
407 pub fn testing_assembler() -> Assembler {
415 let source_manager: Arc<dyn SourceManager + Send + Sync> =
416 Arc::new(DefaultSourceManager::default());
417 let kernel_library = Self::kernel_as_library();
418
419 #[cfg(all(any(feature = "testing", test), feature = "std"))]
420 source_manager_ext::load_masm_source_files(&source_manager);
421
422 Assembler::with_kernel(source_manager, Self::kernel())
423 .with_library(StdLibrary::default())
424 .expect("failed to load std-lib")
425 .with_library(MidenLib::default())
426 .expect("failed to load miden-lib")
427 .with_library(kernel_library)
428 .expect("failed to load kernel library (/lib)")
429 .with_debug_mode(true)
430 }
431
432 pub fn testing_assembler_with_mock_account() -> Assembler {
436 let assembler = Self::testing_assembler().with_debug_mode(true);
437 let library = miden_objects::account::AccountCode::mock_library(assembler.clone());
438
439 assembler.with_library(library).expect("failed to add mock account code")
440 }
441}
442
443#[cfg(all(any(feature = "testing", test), feature = "std"))]
444mod source_manager_ext {
445 use std::{
446 fs, io,
447 path::{Path, PathBuf},
448 sync::Arc,
449 vec::Vec,
450 };
451
452 use miden_objects::assembly::{SourceManager, diagnostics::SourceManagerExt};
453
454 pub fn load_masm_source_files(source_manager: &Arc<dyn SourceManager + Send + Sync>) {
461 if let Err(err) = load(source_manager) {
462 std::eprintln!("failed to load MASM sources into source manager: {err}");
465 }
466 }
467
468 fn load(source_manager: &Arc<dyn SourceManager + Send + Sync>) -> io::Result<()> {
470 for file in get_masm_files(concat!(env!("OUT_DIR"), "/asm"))? {
471 source_manager.load_file(&file).map_err(io::Error::other)?;
472 }
473
474 Ok(())
475 }
476
477 fn get_masm_files<P: AsRef<Path>>(dir_path: P) -> io::Result<Vec<PathBuf>> {
482 let mut files = Vec::new();
483
484 match fs::read_dir(dir_path) {
485 Ok(entries) => {
486 for entry in entries {
487 match entry {
488 Ok(entry) => {
489 let entry_path = entry.path();
490 if entry_path.is_dir() {
491 files.extend(get_masm_files(entry_path)?);
492 } else if entry_path
493 .extension()
494 .map(|ext| ext == "masm")
495 .unwrap_or(false)
496 {
497 files.push(entry_path);
498 }
499 },
500 Err(e) => {
501 return Err(io::Error::other(format!(
502 "error reading directory entry: {e}",
503 )));
504 },
505 }
506 }
507 },
508 Err(e) => {
509 return Err(io::Error::other(format!("error reading directory: {e}")));
510 },
511 }
512
513 Ok(files)
514 }
515}