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