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::transaction::{
13 OutputNote,
14 OutputNotes,
15 TransactionArgs,
16 TransactionInputs,
17 TransactionOutputs,
18};
19use miden_objects::utils::serde::Deserializable;
20use miden_objects::utils::sync::LazyLock;
21use miden_objects::vm::{AdviceInputs, Program, ProgramInfo, StackInputs, StackOutputs};
22use miden_objects::{Felt, Hasher, TransactionOutputError, Word};
23use miden_stdlib::StdLibrary;
24
25use super::MidenLib;
26
27pub mod memory;
28
29mod events;
30pub use events::TransactionEvent;
31
32mod inputs;
33pub use inputs::{TransactionAdviceInputs, TransactionAdviceMapMismatch};
34
35mod outputs;
36pub use outputs::{
37 ACCOUNT_UPDATE_COMMITMENT_WORD_IDX,
38 EXPIRATION_BLOCK_ELEMENT_IDX,
39 FEE_ASSET_WORD_IDX,
40 OUTPUT_NOTES_COMMITMENT_WORD_IDX,
41 parse_final_account_header,
42};
43
44pub use crate::errors::{
45 TransactionEventError,
46 TransactionKernelError,
47 TransactionTraceParsingError,
48};
49
50mod procedures;
51
52static KERNEL_LIB: LazyLock<KernelLibrary> = LazyLock::new(|| {
57 let kernel_lib_bytes =
58 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_kernel.masl"));
59 KernelLibrary::read_from_bytes(kernel_lib_bytes)
60 .expect("failed to deserialize transaction kernel library")
61});
62
63static KERNEL_MAIN: LazyLock<Program> = LazyLock::new(|| {
65 let kernel_main_bytes =
66 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_kernel.masb"));
67 Program::read_from_bytes(kernel_main_bytes)
68 .expect("failed to deserialize transaction kernel runtime")
69});
70
71static TX_SCRIPT_MAIN: LazyLock<Program> = LazyLock::new(|| {
73 let tx_script_main_bytes =
74 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_script_main.masb"));
75 Program::read_from_bytes(tx_script_main_bytes)
76 .expect("failed to deserialize tx script executor runtime")
77});
78
79pub struct TransactionKernel;
83
84impl TransactionKernel {
85 pub fn kernel() -> KernelLibrary {
93 KERNEL_LIB.clone()
94 }
95
96 pub fn main() -> Program {
101 KERNEL_MAIN.clone()
102 }
103
104 pub fn tx_script_main() -> Program {
109 TX_SCRIPT_MAIN.clone()
110 }
111
112 pub fn program_info() -> ProgramInfo {
117 let program_hash = Self::main().hash();
119 let kernel = Self::kernel().kernel().clone();
120
121 ProgramInfo::new(program_hash, kernel)
122 }
123
124 pub fn prepare_inputs(
129 tx_inputs: &TransactionInputs,
130 tx_args: &TransactionArgs,
131 init_advice_inputs: Option<AdviceInputs>,
132 ) -> Result<(StackInputs, TransactionAdviceInputs), TransactionAdviceMapMismatch> {
133 let account = tx_inputs.account();
134
135 let stack_inputs = TransactionKernel::build_input_stack(
136 account.id(),
137 account.init_commitment(),
138 tx_inputs.input_notes().commitment(),
139 tx_inputs.block_header().commitment(),
140 tx_inputs.block_header().block_num(),
141 );
142
143 let mut tx_advice_inputs = TransactionAdviceInputs::new(tx_inputs, tx_args)?;
144 if let Some(init_advice_inputs) = init_advice_inputs {
145 tx_advice_inputs.extend(init_advice_inputs);
146 }
147
148 Ok((stack_inputs, tx_advice_inputs))
149 }
150
151 pub fn assembler() -> Assembler {
157 Self::assembler_with_source_manager(Arc::new(DefaultSourceManager::default()))
158 }
159
160 pub fn assembler_with_source_manager(source_manager: Arc<dyn SourceManagerSync>) -> Assembler {
163 #[cfg(all(any(feature = "testing", test), feature = "std"))]
164 source_manager_ext::load_masm_source_files(&source_manager);
165
166 Assembler::with_kernel(source_manager, Self::kernel())
167 .with_dynamic_library(StdLibrary::default())
168 .expect("failed to load std-lib")
169 .with_dynamic_library(MidenLib::default())
170 .expect("failed to load miden-lib")
171 }
172
173 pub fn build_input_stack(
198 account_id: AccountId,
199 init_account_commitment: Word,
200 input_notes_commitment: Word,
201 block_commitment: Word,
202 block_num: BlockNumber,
203 ) -> StackInputs {
204 let mut inputs: Vec<Felt> = Vec::with_capacity(14);
206 inputs.push(Felt::from(block_num));
207 inputs.push(account_id.suffix());
208 inputs.push(account_id.prefix().as_felt());
209 inputs.extend(input_notes_commitment);
210 inputs.extend_from_slice(init_account_commitment.as_elements());
211 inputs.extend_from_slice(block_commitment.as_elements());
212 StackInputs::new(inputs)
213 .map_err(|e| e.to_string())
214 .expect("Invalid stack input")
215 }
216
217 pub fn build_output_stack(
236 final_account_commitment: Word,
237 account_delta_commitment: Word,
238 output_notes_commitment: Word,
239 fee: FungibleAsset,
240 expiration_block_num: BlockNumber,
241 ) -> StackOutputs {
242 let account_update_commitment =
243 Hasher::merge(&[final_account_commitment, account_delta_commitment]);
244 let mut outputs: Vec<Felt> = Vec::with_capacity(9);
245 outputs.push(Felt::from(expiration_block_num));
246 outputs.extend(Word::from(fee));
247 outputs.extend(account_update_commitment);
248 outputs.extend(output_notes_commitment);
249 outputs.reverse();
250 StackOutputs::new(outputs)
251 .map_err(|e| e.to_string())
252 .expect("Invalid stack output")
253 }
254
255 pub fn parse_output_stack(
283 stack: &StackOutputs,
284 ) -> Result<(Word, Word, FungibleAsset, BlockNumber), TransactionOutputError> {
285 let output_notes_commitment = stack
286 .get_stack_word(OUTPUT_NOTES_COMMITMENT_WORD_IDX * 4)
287 .expect("output_notes_commitment (first word) missing");
288
289 let account_update_commitment = stack
290 .get_stack_word(ACCOUNT_UPDATE_COMMITMENT_WORD_IDX * 4)
291 .expect("account_update_commitment (second word) missing");
292
293 let fee = stack
294 .get_stack_word(FEE_ASSET_WORD_IDX * 4)
295 .expect("fee_asset (third word) missing");
296
297 let expiration_block_num = stack
298 .get_stack_item(EXPIRATION_BLOCK_ELEMENT_IDX)
299 .expect("tx_expiration_block_num (element on index 12) missing");
300
301 let expiration_block_num = u32::try_from(expiration_block_num.as_int())
302 .map_err(|_| {
303 TransactionOutputError::OutputStackInvalid(
304 "expiration block number should be smaller than u32::MAX".into(),
305 )
306 })?
307 .into();
308
309 if stack.get_stack_word(12).expect("fourth word missing").as_elements()[..3]
312 != Word::empty().as_elements()[..3]
313 {
314 return Err(TransactionOutputError::OutputStackInvalid(
315 "indices 13, 14 and 15 on the output stack should be ZERO".into(),
316 ));
317 }
318
319 let fee = FungibleAsset::try_from(fee)
320 .map_err(TransactionOutputError::FeeAssetNotFungibleAsset)?;
321
322 Ok((output_notes_commitment, account_update_commitment, fee, expiration_block_num))
323 }
324
325 pub fn from_transaction_parts(
355 stack: &StackOutputs,
356 advice_inputs: &AdviceInputs,
357 output_notes: Vec<OutputNote>,
358 ) -> Result<TransactionOutputs, TransactionOutputError> {
359 let (output_notes_commitment, account_update_commitment, fee, expiration_block_num) =
360 Self::parse_output_stack(stack)?;
361
362 let (final_account_commitment, account_delta_commitment) =
363 Self::parse_account_update_commitment(account_update_commitment, advice_inputs)?;
364
365 let final_account_data = advice_inputs
367 .map
368 .get(&final_account_commitment)
369 .ok_or(TransactionOutputError::FinalAccountCommitmentMissingInAdviceMap)?;
370
371 let account = parse_final_account_header(final_account_data)
372 .map_err(TransactionOutputError::FinalAccountHeaderParseFailure)?;
373
374 let output_notes = OutputNotes::new(output_notes)?;
376 if output_notes_commitment != output_notes.commitment() {
377 return Err(TransactionOutputError::OutputNotesCommitmentInconsistent {
378 actual: output_notes.commitment(),
379 expected: output_notes_commitment,
380 });
381 }
382
383 Ok(TransactionOutputs {
384 account,
385 account_delta_commitment,
386 output_notes,
387 fee,
388 expiration_block_num,
389 })
390 }
391
392 fn parse_account_update_commitment(
395 account_update_commitment: Word,
396 advice_inputs: &AdviceInputs,
397 ) -> Result<(Word, Word), TransactionOutputError> {
398 let account_update_data =
399 advice_inputs.map.get(&account_update_commitment).ok_or_else(|| {
400 TransactionOutputError::AccountUpdateCommitment(
401 "failed to find ACCOUNT_UPDATE_COMMITMENT in advice map".into(),
402 )
403 })?;
404
405 if account_update_data.len() != 8 {
406 return Err(TransactionOutputError::AccountUpdateCommitment(
407 "expected account update commitment advice map entry to contain exactly 8 elements"
408 .into(),
409 ));
410 }
411
412 let final_account_commitment = Word::from(
415 <[Felt; 4]>::try_from(&account_update_data[0..4])
416 .expect("we should have sliced off exactly four elements"),
417 );
418 let account_delta_commitment = Word::from(
419 <[Felt; 4]>::try_from(&account_update_data[4..8])
420 .expect("we should have sliced off exactly four elements"),
421 );
422
423 let computed_account_update_commitment =
424 Hasher::merge(&[final_account_commitment, account_delta_commitment]);
425
426 if computed_account_update_commitment != account_update_commitment {
427 let err_message = format!(
428 "transaction outputs account update commitment {account_update_commitment} but commitment computed from its advice map entries was {computed_account_update_commitment}"
429 );
430 return Err(TransactionOutputError::AccountUpdateCommitment(err_message.into()));
431 }
432
433 Ok((final_account_commitment, account_delta_commitment))
434 }
435}
436
437#[cfg(any(feature = "testing", test))]
438impl TransactionKernel {
439 const KERNEL_TESTING_LIB_BYTES: &'static [u8] =
440 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/kernel_library.masl"));
441
442 pub fn library() -> Library {
444 Library::read_from_bytes(Self::KERNEL_TESTING_LIB_BYTES)
445 .expect("failed to deserialize transaction kernel library")
446 }
447
448 pub fn mock_libraries() -> impl Iterator<Item = Library> {
450 use miden_objects::account::AccountCode;
451
452 use crate::testing::mock_account_code::MockAccountCodeExt;
453
454 vec![AccountCode::mock_account_library(), AccountCode::mock_faucet_library()].into_iter()
455 }
456
457 pub fn with_kernel_library(source_manager: Arc<dyn SourceManagerSync>) -> Assembler {
465 Self::assembler_with_source_manager(source_manager)
466 .with_dynamic_library(Self::library())
467 .expect("failed to load kernel library (/lib)")
468 .with_debug_mode(true)
469 }
470
471 pub fn with_mock_libraries(source_manager: Arc<dyn SourceManagerSync>) -> Assembler {
481 let mut assembler = Self::with_kernel_library(source_manager);
482
483 for library in Self::mock_libraries() {
484 assembler
485 .link_dynamic_library(library)
486 .expect("failed to add mock account libraries");
487 }
488
489 assembler
490 }
491}
492
493#[cfg(all(any(feature = "testing", test), feature = "std"))]
494mod source_manager_ext {
495 use std::path::{Path, PathBuf};
496 use std::vec::Vec;
497 use std::{fs, io};
498
499 use miden_objects::assembly::SourceManager;
500 use miden_objects::assembly::debuginfo::SourceManagerExt;
501
502 pub fn load_masm_source_files(source_manager: &dyn SourceManager) {
509 if let Err(err) = load(source_manager) {
510 std::eprintln!("failed to load MASM sources into source manager: {err}");
513 }
514 }
515
516 fn load(source_manager: &dyn SourceManager) -> io::Result<()> {
518 for file in get_masm_files(concat!(env!("OUT_DIR"), "/asm"))? {
519 source_manager.load_file(&file).map_err(io::Error::other)?;
520 }
521
522 Ok(())
523 }
524
525 fn get_masm_files<P: AsRef<Path>>(dir_path: P) -> io::Result<Vec<PathBuf>> {
530 let mut files = Vec::new();
531
532 match fs::read_dir(dir_path) {
533 Ok(entries) => {
534 for entry in entries {
535 match entry {
536 Ok(entry) => {
537 let entry_path = entry.path();
538 if entry_path.is_dir() {
539 files.extend(get_masm_files(entry_path)?);
540 } else if entry_path
541 .extension()
542 .map(|ext| ext == "masm")
543 .unwrap_or(false)
544 {
545 files.push(entry_path);
546 }
547 },
548 Err(e) => {
549 return Err(io::Error::other(format!(
550 "error reading directory entry: {e}",
551 )));
552 },
553 }
554 }
555 },
556 Err(e) => {
557 return Err(io::Error::other(format!("error reading directory: {e}")));
558 },
559 }
560
561 Ok(files)
562 }
563}