miden_protocol/transaction/kernel/
mod.rs1use alloc::sync::Arc;
2use alloc::vec::Vec;
3
4use miden_core_lib::CoreLibrary;
5
6use crate::account::{AccountHeader, AccountId};
7use crate::assembly::debuginfo::SourceManagerSync;
8use crate::assembly::{Assembler, DefaultSourceManager, Linkage};
9use crate::block::BlockNumber;
10use crate::crypto::SequentialCommit;
11use crate::errors::TransactionOutputError;
12use crate::protocol::ProtocolLib;
13use crate::transaction::{RawOutputNote, RawOutputNotes, TransactionInputs, TransactionOutputs};
14use crate::utils::sync::LazyLock;
15use crate::vm::{
16 AdviceInputs,
17 DebugSourceNodeId,
18 Package,
19 PackageDebugInfo,
20 Program,
21 ProgramInfo,
22 StackInputs,
23 StackOutputs,
24};
25use crate::{Felt, Hasher, Word};
26
27mod procedures {
28 include!(concat!(env!("OUT_DIR"), "/procedures.rs"));
29}
30
31pub mod memory;
32
33mod advice_inputs;
34mod tx_event_id;
35
36pub use advice_inputs::TransactionAdviceInputs;
37pub use tx_event_id::TransactionEventId;
38
39static KERNEL_PACKAGE: LazyLock<Arc<Package>> = LazyLock::new(|| {
44 let kernel_package_bytes =
45 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/miden-tx-kernel.masp"));
46 Arc::new(
47 Package::read_from_bytes_trusted(kernel_package_bytes)
49 .expect("failed to deserialize transaction kernel package"),
50 )
51});
52
53static KERNEL_MAIN_PACKAGE: LazyLock<Arc<Package>> = LazyLock::new(|| {
55 let kernel_main_bytes =
56 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/miden-tx-kernel:main.masp"));
57 Arc::new(
58 Package::read_from_bytes_trusted(kernel_main_bytes)
60 .expect("failed to deserialize transaction kernel main package"),
61 )
62});
63
64static KERNEL_MAIN: LazyLock<Program> = LazyLock::new(|| {
66 KERNEL_MAIN_PACKAGE
67 .try_into_program()
68 .expect("transaction kernel main package should contain a program")
69});
70
71static TX_SCRIPT_MAIN_PACKAGE: LazyLock<Arc<Package>> = LazyLock::new(|| {
73 let tx_script_main_bytes = include_bytes!(concat!(
74 env!("OUT_DIR"),
75 "/assets/kernels/miden-tx-kernel:tx-script-main.masp"
76 ));
77 Arc::new(
78 Package::read_from_bytes_trusted(tx_script_main_bytes)
80 .expect("failed to deserialize tx script executor package"),
81 )
82});
83
84static TX_SCRIPT_MAIN: LazyLock<Program> = LazyLock::new(|| {
86 TX_SCRIPT_MAIN_PACKAGE
87 .try_into_program()
88 .expect("tx script executor package should contain a program")
89});
90
91static KERNEL_MAIN_DEBUG_INFO: LazyLock<Option<Arc<PackageDebugInfo>>> =
92 LazyLock::new(|| package_debug_info(&KERNEL_MAIN_PACKAGE, "transaction kernel main"));
93
94static TX_SCRIPT_MAIN_DEBUG_INFO: LazyLock<Option<Arc<PackageDebugInfo>>> =
95 LazyLock::new(|| package_debug_info(&TX_SCRIPT_MAIN_PACKAGE, "tx script executor"));
96
97pub struct TransactionKernel;
101
102impl TransactionKernel {
103 pub const PROCEDURES: &'static [Word] = &procedures::KERNEL_PROCEDURES;
108
109 pub fn package() -> Arc<Package> {
114 KERNEL_PACKAGE.clone()
115 }
116
117 pub fn main() -> Program {
122 KERNEL_MAIN.clone()
123 }
124
125 pub fn main_debug_info() -> Option<Arc<PackageDebugInfo>> {
130 KERNEL_MAIN_DEBUG_INFO.clone()
131 }
132
133 pub fn main_entrypoint_source_node() -> Option<DebugSourceNodeId> {
135 package_entrypoint_source_node(&KERNEL_MAIN_PACKAGE, KERNEL_MAIN_DEBUG_INFO.as_deref())
136 }
137
138 pub fn tx_script_main() -> Program {
143 TX_SCRIPT_MAIN.clone()
144 }
145
146 pub fn tx_script_main_debug_info() -> Option<Arc<PackageDebugInfo>> {
152 TX_SCRIPT_MAIN_DEBUG_INFO.clone()
153 }
154
155 pub fn tx_script_main_entrypoint_source_node() -> Option<DebugSourceNodeId> {
157 package_entrypoint_source_node(
158 &TX_SCRIPT_MAIN_PACKAGE,
159 TX_SCRIPT_MAIN_DEBUG_INFO.as_deref(),
160 )
161 }
162
163 pub fn program_info() -> ProgramInfo {
168 let program_hash = Self::main().hash();
170 let kernel = Self::package()
171 .to_kernel()
172 .expect("transaction kernel package should describe a valid kernel");
173
174 ProgramInfo::new(program_hash, kernel)
175 }
176
177 pub fn prepare_inputs(tx_inputs: &TransactionInputs) -> (StackInputs, TransactionAdviceInputs) {
180 let account = tx_inputs.account();
181
182 let stack_inputs = TransactionKernel::build_input_stack(
183 account.id(),
184 account.initial_commitment(),
185 tx_inputs.input_notes().commitment(),
186 tx_inputs.block_header().commitment(),
187 tx_inputs.block_header().block_num(),
188 );
189
190 let tx_advice_inputs = TransactionAdviceInputs::new(tx_inputs);
191
192 (stack_inputs, tx_advice_inputs)
193 }
194
195 pub fn assembler() -> Assembler {
201 Self::assembler_with_source_manager(Arc::new(DefaultSourceManager::default()))
202 }
203
204 pub fn assembler_with_source_manager(source_manager: Arc<dyn SourceManagerSync>) -> Assembler {
207 #[cfg(all(any(feature = "testing", test), feature = "std"))]
208 source_manager_ext::load_masm_source_files(&source_manager);
209
210 let mut assembler = Assembler::with_kernel(source_manager, Self::package())
211 .expect("failed to load transaction kernel");
212 assembler
213 .link_package(CoreLibrary::default().package(), Linkage::Dynamic)
214 .expect("failed to load std-lib");
215 assembler
216 .link_package(ProtocolLib::default().package(), Linkage::Dynamic)
217 .expect("failed to load miden-lib");
218 assembler
219 }
220
221 pub fn build_input_stack(
246 account_id: AccountId,
247 initial_account_commitment: Word,
248 input_notes_commitment: Word,
249 block_commitment: Word,
250 block_num: BlockNumber,
251 ) -> StackInputs {
252 let mut inputs: Vec<Felt> = Vec::with_capacity(14);
254 inputs.extend_from_slice(block_commitment.as_elements());
255 inputs.extend_from_slice(initial_account_commitment.as_elements());
256 inputs.extend(input_notes_commitment);
257 inputs.push(account_id.suffix());
258 inputs.push(account_id.prefix().as_felt());
259 inputs.push(Felt::from(block_num));
260
261 StackInputs::new(&inputs).expect("number of stack inputs should be <= 16")
262 }
263
264 pub fn build_output_stack(
281 final_account_commitment: Word,
282 account_delta_commitment: Word,
283 output_notes_commitment: Word,
284 expiration_block_num: BlockNumber,
285 ) -> StackOutputs {
286 let account_update_commitment =
287 Hasher::merge(&[final_account_commitment, account_delta_commitment]);
288
289 let mut outputs: Vec<Felt> = Vec::with_capacity(9);
290 outputs.extend(output_notes_commitment);
291 outputs.extend(account_update_commitment);
292 outputs.push(Felt::from(expiration_block_num));
293
294 StackOutputs::new(&outputs).expect("number of stack inputs should be <= 16")
295 }
296
297 pub fn parse_output_stack(
323 stack: &StackOutputs, ) -> Result<(Word, Word, BlockNumber), TransactionOutputError> {
325 let output_notes_commitment = stack
326 .get_word(TransactionOutputs::OUTPUT_NOTES_COMMITMENT_WORD_IDX)
327 .expect("output_notes_commitment (first word) missing");
328
329 let account_update_commitment = stack
330 .get_word(TransactionOutputs::ACCOUNT_UPDATE_COMMITMENT_WORD_IDX)
331 .expect("account_update_commitment (second word) missing");
332
333 let expiration_block_num = stack
334 .get_element(TransactionOutputs::EXPIRATION_BLOCK_ELEMENT_IDX)
335 .expect("tx_expiration_block_num missing");
336
337 let expiration_block_num = u32::try_from(expiration_block_num.as_canonical_u64())
338 .map_err(|_| {
339 TransactionOutputError::OutputStackInvalid(
340 "expiration block number should be smaller than u32::MAX".into(),
341 )
342 })?
343 .into();
344
345 if stack.as_slice()[9..].iter().any(|element| *element != Felt::ZERO) {
347 return Err(TransactionOutputError::OutputStackInvalid(
348 "indices 9..16 on the output stack should be ZERO".into(),
349 ));
350 }
351
352 Ok((output_notes_commitment, account_update_commitment, expiration_block_num))
353 }
354
355 pub fn from_transaction_parts(
383 stack: &StackOutputs,
384 advice_inputs: &AdviceInputs,
385 output_notes: Vec<RawOutputNote>,
386 ) -> Result<TransactionOutputs, TransactionOutputError> {
387 let (output_notes_commitment, account_update_commitment, expiration_block_num) =
388 Self::parse_output_stack(stack)?;
389
390 let (final_account_commitment, account_patch_commitment) =
391 Self::parse_account_update_commitment(account_update_commitment, advice_inputs)?;
392
393 let final_account_data = advice_inputs
395 .map
396 .get(&final_account_commitment)
397 .ok_or(TransactionOutputError::FinalAccountCommitmentMissingInAdviceMap)?;
398
399 let account = AccountHeader::try_from_elements(final_account_data)
400 .map_err(TransactionOutputError::FinalAccountHeaderParseFailure)?;
401
402 let output_notes = RawOutputNotes::new(output_notes)?;
404 if output_notes_commitment != output_notes.commitment() {
405 return Err(TransactionOutputError::OutputNotesCommitmentInconsistent {
406 actual: output_notes.commitment(),
407 expected: output_notes_commitment,
408 });
409 }
410
411 Ok(TransactionOutputs::new(
412 account,
413 account_patch_commitment,
414 output_notes,
415 expiration_block_num,
416 ))
417 }
418
419 fn parse_account_update_commitment(
422 account_update_commitment: Word,
423 advice_inputs: &AdviceInputs,
424 ) -> Result<(Word, Word), TransactionOutputError> {
425 let account_update_data =
426 advice_inputs.map.get(&account_update_commitment).ok_or_else(|| {
427 TransactionOutputError::AccountUpdateCommitment(
428 "failed to find ACCOUNT_UPDATE_COMMITMENT in advice map".into(),
429 )
430 })?;
431
432 if account_update_data.len() != 8 {
433 return Err(TransactionOutputError::AccountUpdateCommitment(
434 "expected account update commitment advice map entry to contain exactly 8 elements"
435 .into(),
436 ));
437 }
438
439 let final_account_commitment = Word::from(
442 <[Felt; 4]>::try_from(&account_update_data[0..4])
443 .expect("we should have sliced off exactly four elements"),
444 );
445 let account_patch_commitment = Word::from(
446 <[Felt; 4]>::try_from(&account_update_data[4..8])
447 .expect("we should have sliced off exactly four elements"),
448 );
449
450 let computed_account_update_commitment =
451 Hasher::merge(&[final_account_commitment, account_patch_commitment]);
452
453 if computed_account_update_commitment != account_update_commitment {
454 let err_message = format!(
455 "transaction outputs account update commitment {account_update_commitment} but commitment computed from its advice map entries was {computed_account_update_commitment}"
456 );
457 return Err(TransactionOutputError::AccountUpdateCommitment(err_message.into()));
458 }
459
460 Ok((final_account_commitment, account_patch_commitment))
461 }
462
463 pub fn to_commitment(&self) -> Word {
468 <Self as SequentialCommit>::to_commitment(self)
469 }
470}
471
472fn package_debug_info(package: &Package, package_name: &str) -> Option<Arc<PackageDebugInfo>> {
473 package
474 .debug_info()
475 .unwrap_or_else(|err| panic!("failed to read {package_name} debug info: {err}"))
476 .map(Arc::new)
477}
478
479fn package_entrypoint_source_node(
480 package: &Package,
481 debug_info: Option<&PackageDebugInfo>,
482) -> Option<DebugSourceNodeId> {
483 let source_node_id = package.entrypoint_source_node()?;
484 debug_info?.source_node(source_node_id)?;
485 Some(source_node_id)
486}
487
488#[cfg(any(feature = "testing", test))]
489impl TransactionKernel {
490 const KERNEL_TESTING_PACKAGE_BYTES: &'static [u8] =
491 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/miden-tx-kernel-core.masp"));
492
493 pub fn library() -> Package {
495 Package::read_from_bytes_trusted(Self::KERNEL_TESTING_PACKAGE_BYTES)
497 .expect("failed to deserialize transaction kernel library package")
498 }
499}
500
501impl SequentialCommit for TransactionKernel {
502 type Commitment = Word;
503
504 fn to_elements(&self) -> Vec<Felt> {
506 Word::words_as_elements(Self::PROCEDURES).to_vec()
507 }
508}
509
510#[cfg(all(any(feature = "testing", test), feature = "std"))]
511pub(crate) mod source_manager_ext {
512 use std::path::{Path, PathBuf};
513 use std::vec::Vec;
514 use std::{fs, io};
515
516 use crate::assembly::SourceManager;
517 use crate::assembly::debuginfo::SourceManagerExt;
518
519 pub fn load_masm_source_files(source_manager: &dyn SourceManager) {
526 if let Err(err) = load(source_manager) {
527 std::eprintln!("failed to load MASM sources into source manager: {err}");
530 }
531 }
532
533 fn load(source_manager: &dyn SourceManager) -> io::Result<()> {
535 for file in get_masm_files(concat!(env!("OUT_DIR"), "/asm"))? {
536 source_manager.load_file(&file).map_err(io::Error::other)?;
537 }
538
539 Ok(())
540 }
541
542 fn get_masm_files<P: AsRef<Path>>(dir_path: P) -> io::Result<Vec<PathBuf>> {
547 let mut files = Vec::new();
548
549 match fs::read_dir(dir_path) {
550 Ok(entries) => {
551 for entry in entries {
552 match entry {
553 Ok(entry) => {
554 let entry_path = entry.path();
555 if entry_path.is_dir() {
556 files.extend(get_masm_files(entry_path)?);
557 } else if entry_path
558 .extension()
559 .map(|ext| ext == "masm")
560 .unwrap_or(false)
561 {
562 files.push(entry_path);
563 }
564 },
565 Err(e) => {
566 return Err(io::Error::other(format!(
567 "error reading directory entry: {e}",
568 )));
569 },
570 }
571 }
572 },
573 Err(e) => {
574 return Err(io::Error::other(format!("error reading directory: {e}")));
575 },
576 }
577
578 Ok(files)
579 }
580}