1use alloc::string::ToString;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4
5use miden_objects::account::AccountId;
6#[cfg(any(feature = "testing", test))]
7use miden_objects::assembly::Library;
8use miden_objects::assembly::debuginfo::SourceManagerSync;
9use miden_objects::assembly::{Assembler, DefaultSourceManager, KernelLibrary};
10use miden_objects::asset::FungibleAsset;
11use miden_objects::block::BlockNumber;
12use miden_objects::crypto::SequentialCommit;
13use miden_objects::transaction::{OutputNote, OutputNotes, TransactionInputs, TransactionOutputs};
14use miden_objects::utils::serde::Deserializable;
15use miden_objects::utils::sync::LazyLock;
16use miden_objects::vm::{AdviceInputs, Program, ProgramInfo, StackInputs, StackOutputs};
17use miden_objects::{Felt, Hasher, TransactionOutputError, Word};
18use miden_stdlib::StdLibrary;
19
20use super::MidenLib;
21
22pub mod memory;
23
24mod events;
25pub use events::{EventId, TransactionEvent};
26
27mod inputs;
28pub use inputs::{TransactionAdviceInputs, TransactionAdviceMapMismatch};
29
30mod outputs;
31pub use outputs::{
32 ACCOUNT_UPDATE_COMMITMENT_WORD_IDX,
33 EXPIRATION_BLOCK_ELEMENT_IDX,
34 FEE_ASSET_WORD_IDX,
35 OUTPUT_NOTES_COMMITMENT_WORD_IDX,
36 parse_final_account_header,
37};
38
39pub use crate::errors::{TransactionEventError, TransactionTraceParsingError};
40
41mod kernel_procedures;
42use kernel_procedures::KERNEL_PROCEDURES;
43
44static KERNEL_LIB: LazyLock<KernelLibrary> = LazyLock::new(|| {
49 let kernel_lib_bytes =
50 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_kernel.masl"));
51 KernelLibrary::read_from_bytes(kernel_lib_bytes)
52 .expect("failed to deserialize transaction kernel library")
53});
54
55static KERNEL_MAIN: LazyLock<Program> = LazyLock::new(|| {
57 let kernel_main_bytes =
58 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_kernel.masb"));
59 Program::read_from_bytes(kernel_main_bytes)
60 .expect("failed to deserialize transaction kernel runtime")
61});
62
63static TX_SCRIPT_MAIN: LazyLock<Program> = LazyLock::new(|| {
65 let tx_script_main_bytes =
66 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_script_main.masb"));
67 Program::read_from_bytes(tx_script_main_bytes)
68 .expect("failed to deserialize tx script executor runtime")
69});
70
71pub struct TransactionKernel;
75
76impl TransactionKernel {
77 pub const PROCEDURES: &'static [Word] = &KERNEL_PROCEDURES;
82
83 pub fn kernel() -> KernelLibrary {
91 KERNEL_LIB.clone()
92 }
93
94 pub fn main() -> Program {
99 KERNEL_MAIN.clone()
100 }
101
102 pub fn tx_script_main() -> Program {
107 TX_SCRIPT_MAIN.clone()
108 }
109
110 pub fn program_info() -> ProgramInfo {
115 let program_hash = Self::main().hash();
117 let kernel = Self::kernel().kernel().clone();
118
119 ProgramInfo::new(program_hash, kernel)
120 }
121
122 pub fn prepare_inputs(
125 tx_inputs: &TransactionInputs,
126 ) -> Result<(StackInputs, TransactionAdviceInputs), TransactionAdviceMapMismatch> {
127 let account = tx_inputs.account();
128
129 let stack_inputs = TransactionKernel::build_input_stack(
130 account.id(),
131 account.initial_commitment(),
132 tx_inputs.input_notes().commitment(),
133 tx_inputs.block_header().commitment(),
134 tx_inputs.block_header().block_num(),
135 );
136
137 let tx_advice_inputs = TransactionAdviceInputs::new(tx_inputs)?;
138
139 Ok((stack_inputs, tx_advice_inputs))
140 }
141
142 pub fn assembler() -> Assembler {
148 Self::assembler_with_source_manager(Arc::new(DefaultSourceManager::default()))
149 }
150
151 pub fn assembler_with_source_manager(source_manager: Arc<dyn SourceManagerSync>) -> Assembler {
154 #[cfg(all(any(feature = "testing", test), feature = "std"))]
155 source_manager_ext::load_masm_source_files(&source_manager);
156
157 Assembler::with_kernel(source_manager, Self::kernel())
158 .with_dynamic_library(StdLibrary::default())
159 .expect("failed to load std-lib")
160 .with_dynamic_library(MidenLib::default())
161 .expect("failed to load miden-lib")
162 }
163
164 pub fn build_input_stack(
189 account_id: AccountId,
190 initial_account_commitment: Word,
191 input_notes_commitment: Word,
192 block_commitment: Word,
193 block_num: BlockNumber,
194 ) -> StackInputs {
195 let mut inputs: Vec<Felt> = Vec::with_capacity(14);
197 inputs.push(Felt::from(block_num));
198 inputs.push(account_id.suffix());
199 inputs.push(account_id.prefix().as_felt());
200 inputs.extend(input_notes_commitment);
201 inputs.extend_from_slice(initial_account_commitment.as_elements());
202 inputs.extend_from_slice(block_commitment.as_elements());
203 StackInputs::new(inputs)
204 .map_err(|e| e.to_string())
205 .expect("Invalid stack input")
206 }
207
208 pub fn build_output_stack(
227 final_account_commitment: Word,
228 account_delta_commitment: Word,
229 output_notes_commitment: Word,
230 fee: FungibleAsset,
231 expiration_block_num: BlockNumber,
232 ) -> StackOutputs {
233 let account_update_commitment =
234 Hasher::merge(&[final_account_commitment, account_delta_commitment]);
235 let mut outputs: Vec<Felt> = Vec::with_capacity(9);
236 outputs.push(Felt::from(expiration_block_num));
237 outputs.extend(Word::from(fee));
238 outputs.extend(account_update_commitment);
239 outputs.extend(output_notes_commitment);
240 outputs.reverse();
241 StackOutputs::new(outputs)
242 .map_err(|e| e.to_string())
243 .expect("Invalid stack output")
244 }
245
246 pub fn parse_output_stack(
274 stack: &StackOutputs, ) -> Result<(Word, Word, FungibleAsset, BlockNumber), TransactionOutputError> {
276 let output_notes_commitment = stack
277 .get_stack_word_be(OUTPUT_NOTES_COMMITMENT_WORD_IDX * 4)
278 .expect("output_notes_commitment (first word) missing");
279
280 let account_update_commitment = stack
281 .get_stack_word_be(ACCOUNT_UPDATE_COMMITMENT_WORD_IDX * 4)
282 .expect("account_update_commitment (second word) missing");
283
284 let fee = stack
285 .get_stack_word_be(FEE_ASSET_WORD_IDX * 4)
286 .expect("fee_asset (third word) missing");
287
288 let expiration_block_num = stack
289 .get_stack_item(EXPIRATION_BLOCK_ELEMENT_IDX)
290 .expect("tx_expiration_block_num (element on index 12) missing");
291
292 let expiration_block_num = u32::try_from(expiration_block_num.as_int())
293 .map_err(|_| {
294 TransactionOutputError::OutputStackInvalid(
295 "expiration block number should be smaller than u32::MAX".into(),
296 )
297 })?
298 .into();
299
300 if stack.get_stack_word_be(12).expect("fourth word missing").as_elements()[..3]
303 != Word::empty().as_elements()[..3]
304 {
305 return Err(TransactionOutputError::OutputStackInvalid(
306 "indices 13, 14 and 15 on the output stack should be ZERO".into(),
307 ));
308 }
309
310 let fee = FungibleAsset::try_from(fee)
311 .map_err(TransactionOutputError::FeeAssetNotFungibleAsset)?;
312
313 Ok((output_notes_commitment, account_update_commitment, fee, expiration_block_num))
314 }
315
316 pub fn from_transaction_parts(
346 stack: &StackOutputs,
347 advice_inputs: &AdviceInputs,
348 output_notes: Vec<OutputNote>,
349 ) -> Result<TransactionOutputs, TransactionOutputError> {
350 let (output_notes_commitment, account_update_commitment, fee, expiration_block_num) =
351 Self::parse_output_stack(stack)?;
352
353 let (final_account_commitment, account_delta_commitment) =
354 Self::parse_account_update_commitment(account_update_commitment, advice_inputs)?;
355
356 let final_account_data = advice_inputs
358 .map
359 .get(&final_account_commitment)
360 .ok_or(TransactionOutputError::FinalAccountCommitmentMissingInAdviceMap)?;
361
362 let account = parse_final_account_header(final_account_data)
363 .map_err(TransactionOutputError::FinalAccountHeaderParseFailure)?;
364
365 let output_notes = OutputNotes::new(output_notes)?;
367 if output_notes_commitment != output_notes.commitment() {
368 return Err(TransactionOutputError::OutputNotesCommitmentInconsistent {
369 actual: output_notes.commitment(),
370 expected: output_notes_commitment,
371 });
372 }
373
374 Ok(TransactionOutputs {
375 account,
376 account_delta_commitment,
377 output_notes,
378 fee,
379 expiration_block_num,
380 })
381 }
382
383 fn parse_account_update_commitment(
386 account_update_commitment: Word,
387 advice_inputs: &AdviceInputs,
388 ) -> Result<(Word, Word), TransactionOutputError> {
389 let account_update_data =
390 advice_inputs.map.get(&account_update_commitment).ok_or_else(|| {
391 TransactionOutputError::AccountUpdateCommitment(
392 "failed to find ACCOUNT_UPDATE_COMMITMENT in advice map".into(),
393 )
394 })?;
395
396 if account_update_data.len() != 8 {
397 return Err(TransactionOutputError::AccountUpdateCommitment(
398 "expected account update commitment advice map entry to contain exactly 8 elements"
399 .into(),
400 ));
401 }
402
403 let final_account_commitment = Word::from(
406 <[Felt; 4]>::try_from(&account_update_data[0..4])
407 .expect("we should have sliced off exactly four elements"),
408 );
409 let account_delta_commitment = Word::from(
410 <[Felt; 4]>::try_from(&account_update_data[4..8])
411 .expect("we should have sliced off exactly four elements"),
412 );
413
414 let computed_account_update_commitment =
415 Hasher::merge(&[final_account_commitment, account_delta_commitment]);
416
417 if computed_account_update_commitment != account_update_commitment {
418 let err_message = format!(
419 "transaction outputs account update commitment {account_update_commitment} but commitment computed from its advice map entries was {computed_account_update_commitment}"
420 );
421 return Err(TransactionOutputError::AccountUpdateCommitment(err_message.into()));
422 }
423
424 Ok((final_account_commitment, account_delta_commitment))
425 }
426
427 pub fn to_commitment(&self) -> Word {
432 <Self as SequentialCommit>::to_commitment(self)
433 }
434}
435
436#[cfg(any(feature = "testing", test))]
437impl TransactionKernel {
438 const KERNEL_TESTING_LIB_BYTES: &'static [u8] =
439 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/kernel_library.masl"));
440
441 pub fn library() -> Library {
443 Library::read_from_bytes(Self::KERNEL_TESTING_LIB_BYTES)
444 .expect("failed to deserialize transaction kernel library")
445 }
446
447 pub fn mock_libraries() -> impl Iterator<Item = Library> {
449 use miden_objects::account::AccountCode;
450
451 use crate::testing::mock_account_code::MockAccountCodeExt;
452
453 vec![AccountCode::mock_account_library(), AccountCode::mock_faucet_library()].into_iter()
454 }
455
456 pub fn with_kernel_library(source_manager: Arc<dyn SourceManagerSync>) -> Assembler {
464 Self::assembler_with_source_manager(source_manager)
465 .with_dynamic_library(Self::library())
466 .expect("failed to load kernel library (/lib)")
467 .with_debug_mode(true)
468 }
469
470 pub fn with_mock_libraries(source_manager: Arc<dyn SourceManagerSync>) -> Assembler {
482 use crate::testing::mock_util_lib::mock_util_library;
483
484 let mut assembler = Self::with_kernel_library(source_manager);
485
486 for library in Self::mock_libraries() {
487 assembler
488 .link_dynamic_library(library)
489 .expect("failed to add mock account libraries");
490 }
491
492 assembler
493 .link_static_library(mock_util_library())
494 .expect("failed to add mock test library");
495
496 assembler
497 }
498}
499
500impl SequentialCommit for TransactionKernel {
501 type Commitment = Word;
502
503 fn to_elements(&self) -> Vec<Felt> {
505 Word::words_as_elements(Self::PROCEDURES).to_vec()
506 }
507}
508
509#[cfg(all(any(feature = "testing", test), feature = "std"))]
510mod source_manager_ext {
511 use std::path::{Path, PathBuf};
512 use std::vec::Vec;
513 use std::{fs, io};
514
515 use miden_objects::assembly::SourceManager;
516 use miden_objects::assembly::debuginfo::SourceManagerExt;
517
518 pub fn load_masm_source_files(source_manager: &dyn SourceManager) {
525 if let Err(err) = load(source_manager) {
526 std::eprintln!("failed to load MASM sources into source manager: {err}");
529 }
530 }
531
532 fn load(source_manager: &dyn SourceManager) -> io::Result<()> {
534 for file in get_masm_files(concat!(env!("OUT_DIR"), "/asm"))? {
535 source_manager.load_file(&file).map_err(io::Error::other)?;
536 }
537
538 Ok(())
539 }
540
541 fn get_masm_files<P: AsRef<Path>>(dir_path: P) -> io::Result<Vec<PathBuf>> {
546 let mut files = Vec::new();
547
548 match fs::read_dir(dir_path) {
549 Ok(entries) => {
550 for entry in entries {
551 match entry {
552 Ok(entry) => {
553 let entry_path = entry.path();
554 if entry_path.is_dir() {
555 files.extend(get_masm_files(entry_path)?);
556 } else if entry_path
557 .extension()
558 .map(|ext| ext == "masm")
559 .unwrap_or(false)
560 {
561 files.push(entry_path);
562 }
563 },
564 Err(e) => {
565 return Err(io::Error::other(format!(
566 "error reading directory entry: {e}",
567 )));
568 },
569 }
570 }
571 },
572 Err(e) => {
573 return Err(io::Error::other(format!("error reading directory: {e}")));
574 },
575 }
576
577 Ok(files)
578 }
579}