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, KernelLibrary};
11use crate::asset::FungibleAsset;
12use crate::block::BlockNumber;
13use crate::crypto::SequentialCommit;
14use crate::errors::TransactionOutputError;
15use crate::protocol::ProtocolLib;
16use crate::transaction::{RawOutputNote, RawOutputNotes, TransactionInputs, TransactionOutputs};
17use crate::utils::serde::Deserializable;
18use crate::utils::sync::LazyLock;
19use crate::vm::{AdviceInputs, Program, ProgramInfo, StackInputs, StackOutputs};
20use crate::{Felt, Hasher, Word};
21
22mod procedures {
23 include!(concat!(env!("OUT_DIR"), "/procedures.rs"));
24}
25
26pub mod memory;
27
28mod advice_inputs;
29mod tx_event_id;
30
31pub use advice_inputs::TransactionAdviceInputs;
32pub use tx_event_id::TransactionEventId;
33
34static KERNEL_LIB: LazyLock<KernelLibrary> = LazyLock::new(|| {
39 let kernel_lib_bytes =
40 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_kernel.masl"));
41 KernelLibrary::read_from_bytes(kernel_lib_bytes)
42 .expect("failed to deserialize transaction kernel library")
43});
44
45static KERNEL_MAIN: LazyLock<Program> = LazyLock::new(|| {
47 let kernel_main_bytes =
48 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_kernel.masb"));
49 Program::read_from_bytes(kernel_main_bytes)
50 .expect("failed to deserialize transaction kernel runtime")
51});
52
53static TX_SCRIPT_MAIN: LazyLock<Program> = LazyLock::new(|| {
55 let tx_script_main_bytes =
56 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/tx_script_main.masb"));
57 Program::read_from_bytes(tx_script_main_bytes)
58 .expect("failed to deserialize tx script executor runtime")
59});
60
61pub struct TransactionKernel;
65
66impl TransactionKernel {
67 pub const PROCEDURES: &'static [Word] = &procedures::KERNEL_PROCEDURES;
72
73 pub fn kernel() -> KernelLibrary {
81 KERNEL_LIB.clone()
82 }
83
84 pub fn main() -> Program {
89 KERNEL_MAIN.clone()
90 }
91
92 pub fn tx_script_main() -> Program {
97 TX_SCRIPT_MAIN.clone()
98 }
99
100 pub fn program_info() -> ProgramInfo {
105 let program_hash = Self::main().hash();
107 let kernel = Self::kernel().kernel().clone();
108
109 ProgramInfo::new(program_hash, kernel)
110 }
111
112 pub fn prepare_inputs(tx_inputs: &TransactionInputs) -> (StackInputs, TransactionAdviceInputs) {
115 let account = tx_inputs.account();
116
117 let stack_inputs = TransactionKernel::build_input_stack(
118 account.id(),
119 account.initial_commitment(),
120 tx_inputs.input_notes().commitment(),
121 tx_inputs.block_header().commitment(),
122 tx_inputs.block_header().block_num(),
123 );
124
125 let tx_advice_inputs = TransactionAdviceInputs::new(tx_inputs);
126
127 (stack_inputs, tx_advice_inputs)
128 }
129
130 pub fn assembler() -> Assembler {
136 Self::assembler_with_source_manager(Arc::new(DefaultSourceManager::default()))
137 }
138
139 pub fn assembler_with_source_manager(source_manager: Arc<dyn SourceManagerSync>) -> Assembler {
142 #[cfg(all(any(feature = "testing", test), feature = "std"))]
143 source_manager_ext::load_masm_source_files(&source_manager);
144
145 Assembler::with_kernel(source_manager, Self::kernel())
146 .with_dynamic_library(CoreLibrary::default())
147 .expect("failed to load std-lib")
148 .with_dynamic_library(ProtocolLib::default())
149 .expect("failed to load miden-lib")
150 }
151
152 pub fn build_input_stack(
177 account_id: AccountId,
178 initial_account_commitment: Word,
179 input_notes_commitment: Word,
180 block_commitment: Word,
181 block_num: BlockNumber,
182 ) -> StackInputs {
183 let mut inputs: Vec<Felt> = Vec::with_capacity(14);
185 inputs.extend_from_slice(block_commitment.as_elements());
186 inputs.extend_from_slice(initial_account_commitment.as_elements());
187 inputs.extend(input_notes_commitment);
188 inputs.push(account_id.suffix());
189 inputs.push(account_id.prefix().as_felt());
190 inputs.push(Felt::from(block_num));
191
192 StackInputs::new(&inputs).expect("number of stack inputs should be <= 16")
193 }
194
195 pub fn build_output_stack(
213 final_account_commitment: Word,
214 account_delta_commitment: Word,
215 output_notes_commitment: Word,
216 fee: FungibleAsset,
217 expiration_block_num: BlockNumber,
218 ) -> StackOutputs {
219 let account_update_commitment =
220 Hasher::merge(&[final_account_commitment, account_delta_commitment]);
221
222 let mut outputs: Vec<Felt> = Vec::with_capacity(12);
223 outputs.extend(output_notes_commitment);
224 outputs.extend(account_update_commitment);
225 outputs.push(fee.faucet_id().suffix());
226 outputs.push(fee.faucet_id().prefix().as_felt());
227 outputs.push(Felt::try_from(fee.amount()).expect("amount should fit into felt"));
228 outputs.push(Felt::from(expiration_block_num));
229
230 StackOutputs::new(&outputs).expect("number of stack inputs should be <= 16")
231 }
232
233 pub fn parse_output_stack(
261 stack: &StackOutputs, ) -> Result<(Word, Word, FungibleAsset, BlockNumber), TransactionOutputError> {
263 let output_notes_commitment = stack
264 .get_word(TransactionOutputs::OUTPUT_NOTES_COMMITMENT_WORD_IDX)
265 .expect("output_notes_commitment (first word) missing");
266
267 let account_update_commitment = stack
268 .get_word(TransactionOutputs::ACCOUNT_UPDATE_COMMITMENT_WORD_IDX)
269 .expect("account_update_commitment (second word) missing");
270
271 let native_asset_id_prefix = stack
272 .get_element(TransactionOutputs::NATIVE_ASSET_ID_PREFIX_ELEMENT_IDX)
273 .expect("native_asset_id_prefix missing");
274 let native_asset_id_suffix = stack
275 .get_element(TransactionOutputs::NATIVE_ASSET_ID_SUFFIX_ELEMENT_IDX)
276 .expect("native_asset_id_suffix missing");
277 let fee_amount = stack
278 .get_element(TransactionOutputs::FEE_AMOUNT_ELEMENT_IDX)
279 .expect("fee_amount missing");
280
281 let expiration_block_num = stack
282 .get_element(TransactionOutputs::EXPIRATION_BLOCK_ELEMENT_IDX)
283 .expect("tx_expiration_block_num missing");
284
285 let expiration_block_num = u32::try_from(expiration_block_num.as_canonical_u64())
286 .map_err(|_| {
287 TransactionOutputError::OutputStackInvalid(
288 "expiration block number should be smaller than u32::MAX".into(),
289 )
290 })?
291 .into();
292
293 if stack.get_word(12).expect("fourth word missing").as_elements()[..3]
296 != Word::empty().as_elements()[..3]
297 {
298 return Err(TransactionOutputError::OutputStackInvalid(
299 "indices 13, 14 and 15 on the output stack should be ZERO".into(),
300 ));
301 }
302
303 let native_asset_id =
304 AccountId::try_from_elements(native_asset_id_suffix, native_asset_id_prefix)
305 .expect("native asset ID should be validated by the tx kernel");
306 let fee = FungibleAsset::new(native_asset_id, fee_amount.as_canonical_u64())
307 .map_err(TransactionOutputError::FeeAssetNotFungibleAsset)?;
308
309 Ok((output_notes_commitment, account_update_commitment, fee, expiration_block_num))
310 }
311
312 pub fn from_transaction_parts(
342 stack: &StackOutputs,
343 advice_inputs: &AdviceInputs,
344 output_notes: Vec<RawOutputNote>,
345 ) -> Result<TransactionOutputs, TransactionOutputError> {
346 let (output_notes_commitment, account_update_commitment, fee, expiration_block_num) =
347 Self::parse_output_stack(stack)?;
348
349 let (final_account_commitment, account_delta_commitment) =
350 Self::parse_account_update_commitment(account_update_commitment, advice_inputs)?;
351
352 let final_account_data = advice_inputs
354 .map
355 .get(&final_account_commitment)
356 .ok_or(TransactionOutputError::FinalAccountCommitmentMissingInAdviceMap)?;
357
358 let account = AccountHeader::try_from_elements(final_account_data)
359 .map_err(TransactionOutputError::FinalAccountHeaderParseFailure)?;
360
361 let output_notes = RawOutputNotes::new(output_notes)?;
363 if output_notes_commitment != output_notes.commitment() {
364 return Err(TransactionOutputError::OutputNotesCommitmentInconsistent {
365 actual: output_notes.commitment(),
366 expected: output_notes_commitment,
367 });
368 }
369
370 Ok(TransactionOutputs {
371 account,
372 account_delta_commitment,
373 output_notes,
374 fee,
375 expiration_block_num,
376 })
377 }
378
379 fn parse_account_update_commitment(
382 account_update_commitment: Word,
383 advice_inputs: &AdviceInputs,
384 ) -> Result<(Word, Word), TransactionOutputError> {
385 let account_update_data =
386 advice_inputs.map.get(&account_update_commitment).ok_or_else(|| {
387 TransactionOutputError::AccountUpdateCommitment(
388 "failed to find ACCOUNT_UPDATE_COMMITMENT in advice map".into(),
389 )
390 })?;
391
392 if account_update_data.len() != 8 {
393 return Err(TransactionOutputError::AccountUpdateCommitment(
394 "expected account update commitment advice map entry to contain exactly 8 elements"
395 .into(),
396 ));
397 }
398
399 let final_account_commitment = Word::from(
402 <[Felt; 4]>::try_from(&account_update_data[0..4])
403 .expect("we should have sliced off exactly four elements"),
404 );
405 let account_delta_commitment = Word::from(
406 <[Felt; 4]>::try_from(&account_update_data[4..8])
407 .expect("we should have sliced off exactly four elements"),
408 );
409
410 let computed_account_update_commitment =
411 Hasher::merge(&[final_account_commitment, account_delta_commitment]);
412
413 if computed_account_update_commitment != account_update_commitment {
414 let err_message = format!(
415 "transaction outputs account update commitment {account_update_commitment} but commitment computed from its advice map entries was {computed_account_update_commitment}"
416 );
417 return Err(TransactionOutputError::AccountUpdateCommitment(err_message.into()));
418 }
419
420 Ok((final_account_commitment, account_delta_commitment))
421 }
422
423 pub fn to_commitment(&self) -> Word {
428 <Self as SequentialCommit>::to_commitment(self)
429 }
430}
431
432#[cfg(any(feature = "testing", test))]
433impl TransactionKernel {
434 const KERNEL_TESTING_LIB_BYTES: &'static [u8] =
435 include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/kernel_library.masl"));
436
437 pub fn library() -> Library {
439 Library::read_from_bytes(Self::KERNEL_TESTING_LIB_BYTES)
440 .expect("failed to deserialize transaction kernel library")
441 }
442}
443
444impl SequentialCommit for TransactionKernel {
445 type Commitment = Word;
446
447 fn to_elements(&self) -> Vec<Felt> {
449 Word::words_as_elements(Self::PROCEDURES).to_vec()
450 }
451}
452
453#[cfg(all(any(feature = "testing", test), feature = "std"))]
454pub(crate) mod source_manager_ext {
455 use std::path::{Path, PathBuf};
456 use std::vec::Vec;
457 use std::{fs, io};
458
459 use crate::assembly::SourceManager;
460 use crate::assembly::debuginfo::SourceManagerExt;
461
462 pub fn load_masm_source_files(source_manager: &dyn SourceManager) {
469 if let Err(err) = load(source_manager) {
470 std::eprintln!("failed to load MASM sources into source manager: {err}");
473 }
474 }
475
476 fn load(source_manager: &dyn SourceManager) -> io::Result<()> {
478 for file in get_masm_files(concat!(env!("OUT_DIR"), "/asm"))? {
479 source_manager.load_file(&file).map_err(io::Error::other)?;
480 }
481
482 Ok(())
483 }
484
485 fn get_masm_files<P: AsRef<Path>>(dir_path: P) -> io::Result<Vec<PathBuf>> {
490 let mut files = Vec::new();
491
492 match fs::read_dir(dir_path) {
493 Ok(entries) => {
494 for entry in entries {
495 match entry {
496 Ok(entry) => {
497 let entry_path = entry.path();
498 if entry_path.is_dir() {
499 files.extend(get_masm_files(entry_path)?);
500 } else if entry_path
501 .extension()
502 .map(|ext| ext == "masm")
503 .unwrap_or(false)
504 {
505 files.push(entry_path);
506 }
507 },
508 Err(e) => {
509 return Err(io::Error::other(format!(
510 "error reading directory entry: {e}",
511 )));
512 },
513 }
514 }
515 },
516 Err(e) => {
517 return Err(io::Error::other(format!("error reading directory: {e}")));
518 },
519 }
520
521 Ok(files)
522 }
523}