diff --git a/Cargo.lock b/Cargo.lock index 5d7cd1389c..c19d949a29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1770,6 +1770,7 @@ dependencies = [ name = "miden-tx-batch-prover" version = "0.14.0" dependencies = [ + "miden-air", "miden-protocol", "miden-tx", ] diff --git a/crates/miden-protocol/asm/kernels/batch/lib/account.masm b/crates/miden-protocol/asm/kernels/batch/lib/account.masm new file mode 100644 index 0000000000..12beb0fb69 --- /dev/null +++ b/crates/miden-protocol/asm/kernels/batch/lib/account.masm @@ -0,0 +1,36 @@ +#! Batch Kernel Account Module +#! +#! This module handles account validation for the batch: +#! - Validating account state transitions (A->B->C ordering) +#! - Checking that the same account's transactions are properly ordered +#! - Validating account update count is within limits + +use $kernel::memory + +# CONSTANTS +# ================================================================================================= + +# Maximum number of unique accounts per batch +const MAX_ACCOUNTS_PER_BATCH=1024 + +# Error constants +const ERR_BATCH_INVALID_ACCOUNT_TRANSITION="invalid account state transition in batch" +const ERR_BATCH_TOO_MANY_ACCOUNTS="batch exceeds maximum unique accounts" + +# ACCOUNT VALIDATION +# ================================================================================================= + +#! Validates all account updates in the batch. +#! +#! For accounts that appear in multiple transactions, validates that: +#! - Transactions are ordered correctly (tx1.final == tx2.init) +#! - State transitions form a valid chain +#! +#! Also validates total unique account count is within limits. +#! +#! Inputs: [] +#! Outputs: [] +pub proc validate_account_updates + # TODO: Implement account validation + push.0 drop +end diff --git a/crates/miden-protocol/asm/kernels/batch/lib/epilogue.masm b/crates/miden-protocol/asm/kernels/batch/lib/epilogue.masm new file mode 100644 index 0000000000..d31d32ab31 --- /dev/null +++ b/crates/miden-protocol/asm/kernels/batch/lib/epilogue.masm @@ -0,0 +1,34 @@ +#! Batch Kernel Epilogue +#! +#! Finalizes the batch and prepares output stack. + +use $kernel::memory +use miden::core::sys + +# EPILOGUE +# ================================================================================================= + +#! Computes output commitments and builds the output stack. +#! +#! Output stack layout (positions 0-15, top to bottom): +#! [INPUT_NOTES_COMMITMENT(4), OUTPUT_NOTES_SMT_ROOT(4), batch_expiration(1), zeros(7)] +#! +#! Inputs: +#! Operand stack: [] +#! Outputs: +#! Operand stack: [INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_SMT_ROOT, batch_expiration_block_num, ...] +pub proc finalize_batch + # Build output stack by pushing values (last pushed = top) + exec.memory::get_batch_expiration_block_num + # => [batch_expiration_block_num] + + padw # TODO: compute OUTPUT_NOTES_SMT_ROOT + # => [OUTPUT_NOTES_SMT_ROOT, batch_expiration_block_num] + + padw # TODO: compute INPUT_NOTES_COMMITMENT + # => [INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_SMT_ROOT, batch_expiration_block_num] + + # Ensure exactly 16 elements on stack + exec.sys::truncate_stack + # => [INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_SMT_ROOT, batch_expiration_block_num, 0, 0, 0, 0, 0, 0, 0] +end diff --git a/crates/miden-protocol/asm/kernels/batch/lib/memory.masm b/crates/miden-protocol/asm/kernels/batch/lib/memory.masm new file mode 100644 index 0000000000..8d6a838782 --- /dev/null +++ b/crates/miden-protocol/asm/kernels/batch/lib/memory.masm @@ -0,0 +1,283 @@ +#! Batch Kernel Memory Layout +#! +#! This module defines the memory layout for the batch kernel and provides +#! accessors for reading/writing data to specific memory regions. +#! +#! Memory is organized into several regions: +#! - Block data (block hash preimage, chain commitment, etc.) +#! - Transaction data (tx_ids, account_ids, proofs metadata) +#! - Note data (input notes, output notes) +#! - Account update data +#! - Temporary/scratch space + +# MEMORY LAYOUT CONSTANTS +# ================================================================================================= + +# Block data region: stores block header fields +const BLOCK_DATA_PTR=100 + +# Transaction data region: stores transaction list +const TX_DATA_PTR=200 + +# Input notes region +const INPUT_NOTES_PTR=1000 + +# Output notes region +const OUTPUT_NOTES_PTR=2000 + +# Account updates region +const ACCOUNT_UPDATES_PTR=3000 + +# Bookkeeping +const TX_COUNT_PTR=50 +const BATCH_EXPIRATION_PTR=51 + +# Temporary storage for TRANSACTIONS_COMMITMENT verification +const SAVED_TX_COMMITMENT_PTR=52 + +# Temporary buffer for hash computation +# Used to store [(TX_ID, account_id_prefix, account_id_suffix, 0, 0), ...] for hashing +# Max 4 txs * 8 felts = 32 felts = 8 words, so addresses 60-91 +const HASH_BUFFER_PTR=60 + +# BLOCK DATA ACCESSORS +# ================================================================================================= + +#! Returns the reference block hash from memory. +#! +#! Inputs: [] +#! Outputs: [BLOCK_HASH] +pub proc get_block_hash + padw push.BLOCK_DATA_PTR mem_loadw_be +end + +#! Stores the reference block hash in memory. +#! +#! Inputs: [BLOCK_HASH] +#! Outputs: [] +pub proc set_block_hash + push.BLOCK_DATA_PTR mem_storew_be dropw +end + +#! Returns the reference block number from memory. +#! +#! Inputs: [] +#! Outputs: [block_num] +pub proc get_block_num + push.BLOCK_DATA_PTR push.4 add mem_load +end + +#! Stores the reference block number in memory. +#! +#! Inputs: [block_num] +#! Outputs: [] +pub proc set_block_num + push.BLOCK_DATA_PTR push.4 add mem_store +end + +#! Returns the chain commitment from memory. +#! +#! Inputs: [] +#! Outputs: [CHAIN_COMMITMENT] +pub proc get_chain_commitment + padw push.BLOCK_DATA_PTR push.8 add mem_loadw_be +end + +#! Stores the chain commitment in memory. +#! +#! Inputs: [CHAIN_COMMITMENT] +#! Outputs: [] +pub proc set_chain_commitment + push.BLOCK_DATA_PTR push.8 add mem_storew_be dropw +end + +# TRANSACTION DATA ACCESSORS +# ================================================================================================= + +#! Returns the number of transactions in the batch. +#! +#! Inputs: [] +#! Outputs: [tx_count] +pub proc get_transaction_count + push.TX_COUNT_PTR mem_load +end + +#! Stores the number of transactions in the batch. +#! +#! Inputs: [tx_count] +#! Outputs: [] +pub proc set_transaction_count + push.TX_COUNT_PTR mem_store +end + +#! Returns the pointer to transaction data for the given index. +#! +#! Inputs: [tx_index] +#! Outputs: [tx_data_ptr] +#! +#! Each transaction entry takes 12 words: +#! - Word 0: TX_ID +#! - Word 1: account_id_prefix, account_id_suffix, expiration_block_num, ref_block_num +#! - Words 2-3: INIT_ACCOUNT_COMMITMENT +#! - Words 4-5: FINAL_ACCOUNT_COMMITMENT +#! - Words 6-7: INPUT_NOTES_COMMITMENT +#! - Words 8-9: OUTPUT_NOTES_COMMITMENT +#! - Words 10-11: reserved +pub proc get_tx_data_ptr + # tx_data_ptr = TX_DATA_PTR + tx_index * 48 (12 words * 4 felts) + push.48 mul push.TX_DATA_PTR add +end + +# BATCH EXPIRATION +# ================================================================================================= + +#! Returns the batch expiration block number. +#! +#! Inputs: [] +#! Outputs: [batch_expiration_block_num] +pub proc get_batch_expiration_block_num + push.BATCH_EXPIRATION_PTR mem_load +end + +#! Stores the batch expiration block number. +#! +#! Inputs: [batch_expiration_block_num] +#! Outputs: [] +pub proc set_batch_expiration_block_num + push.BATCH_EXPIRATION_PTR mem_store +end + +#! Returns the saved TRANSACTIONS_COMMITMENT from memory. +#! +#! Inputs: [] +#! Outputs: [TRANSACTIONS_COMMITMENT] +pub proc get_saved_tx_commitment + padw push.SAVED_TX_COMMITMENT_PTR mem_loadw_be +end + +#! Stores the TRANSACTIONS_COMMITMENT in memory for later verification. +#! +#! Inputs: [TRANSACTIONS_COMMITMENT] +#! Outputs: [] +pub proc set_saved_tx_commitment + push.SAVED_TX_COMMITMENT_PTR mem_storew_be dropw +end + +#! Returns the hash buffer pointer. +#! +#! Inputs: [] +#! Outputs: [hash_buffer_ptr] +pub proc get_hash_buffer_ptr + push.HASH_BUFFER_PTR +end + +# NOTE DATA ACCESSORS +# ================================================================================================= + +#! Returns the pointer to input notes data. +#! +#! Inputs: [] +#! Outputs: [input_notes_ptr] +pub proc get_input_notes_ptr + push.INPUT_NOTES_PTR +end + +#! Returns the pointer to output notes data. +#! +#! Inputs: [] +#! Outputs: [output_notes_ptr] +pub proc get_output_notes_ptr + push.OUTPUT_NOTES_PTR +end + +# ACCOUNT UPDATES ACCESSORS +# ================================================================================================= + +#! Returns the pointer to account updates data. +#! +#! Inputs: [] +#! Outputs: [account_updates_ptr] +pub proc get_account_updates_ptr + push.ACCOUNT_UPDATES_PTR +end + +# LINK MAP MEMORY REGION +# ================================================================================================= +# +# The link map is used for sorted data structures (account deltas, notes). +# Each entry takes 16 field elements (4 words). + +# Error when link map memory is exhausted +const ERR_LINK_MAP_MAX_ENTRIES_EXCEEDED="number of link map entries exceeds maximum" + +# The inclusive start of the link map dynamic memory region. +# Chosen as a number greater than 2^25 such that all entry pointers are multiples of +# LINK_MAP_ENTRY_SIZE. That enables a simpler check in assert_entry_ptr_is_valid. +const LINK_MAP_REGION_START_PTR=33554448 + +# The non-inclusive end of the link map dynamic memory region. +# This happens to be 2^26, but if it is changed, it should be chosen as a number such that +# LINK_MAP_REGION_END_PTR - LINK_MAP_REGION_START_PTR is a multiple of LINK_MAP_ENTRY_SIZE, +# because that enables checking whether a newly allocated entry pointer is at the end of the range +# using equality rather than lt/gt in link_map_malloc. +const LINK_MAP_REGION_END_PTR=67108864 + +# LINK_MAP_REGION_START_PTR + the currently used size stored at this pointer defines the next +# entry pointer that will be allocated. +const LINK_MAP_USED_MEMORY_SIZE=33554432 + +# The size of each map entry, i.e. four words. +const LINK_MAP_ENTRY_SIZE=16 + +#! Returns the link map memory start ptr constant. +#! +#! Inputs: [] +#! Outputs: [start_ptr] +pub proc get_link_map_region_start_ptr + push.LINK_MAP_REGION_START_PTR +end + +#! Returns the link map memory end ptr constant. +#! +#! Inputs: [] +#! Outputs: [end_ptr] +pub proc get_link_map_region_end_ptr + push.LINK_MAP_REGION_END_PTR +end + +#! Returns the link map entry size constant. +#! +#! Inputs: [] +#! Outputs: [entry_size] +pub proc get_link_map_entry_size + push.LINK_MAP_ENTRY_SIZE +end + +#! Returns the next pointer to an empty link map entry. +#! +#! Inputs: [] +#! Outputs: [entry_ptr] +#! +#! Panics if: +#! - the allocation exceeds the maximum possible number of link map entries. +pub proc link_map_malloc + # retrieve the current memory size + mem_load.LINK_MAP_USED_MEMORY_SIZE dup + # => [current_mem_size, current_mem_size] + + # store next offset + add.LINK_MAP_ENTRY_SIZE + # => [next_mem_size, current_mem_size] + + mem_store.LINK_MAP_USED_MEMORY_SIZE + # => [current_mem_size] + + add.LINK_MAP_REGION_START_PTR + # => [entry_ptr] + + # If entry_ptr is the end_ptr the entry would be allocated in the next memory region so + # we must abort. + # We can use neq because of how the end ptr is chosen. See its docs for details. + dup neq.LINK_MAP_REGION_END_PTR assert.err=ERR_LINK_MAP_MAX_ENTRIES_EXCEEDED + # => [entry_ptr] +end diff --git a/crates/miden-protocol/asm/kernels/batch/lib/note.masm b/crates/miden-protocol/asm/kernels/batch/lib/note.masm new file mode 100644 index 0000000000..d39198ef2c --- /dev/null +++ b/crates/miden-protocol/asm/kernels/batch/lib/note.masm @@ -0,0 +1,45 @@ +#! Batch Kernel Note Module +#! +#! This module handles note validation for the batch: +#! - Collecting input notes from all transactions +#! - Collecting output notes from all transactions +#! - Checking for duplicate input notes (same nullifier) +#! - Checking for duplicate output notes (same note_id) +#! - Erasing consumed unauthenticated notes (output -> input cancellation) +#! - Validating note counts are within limits + +use $kernel::memory + +# CONSTANTS +# ================================================================================================= + +# Maximum input notes per batch +const MAX_INPUT_NOTES_PER_BATCH=256 + +# Maximum output notes per batch +const MAX_OUTPUT_NOTES_PER_BATCH=256 + +# Error constants +const ERR_BATCH_DUPLICATE_INPUT_NOTE="duplicate input note (nullifier) in batch" +const ERR_BATCH_TOO_MANY_INPUT_NOTES="batch exceeds maximum input notes" +const ERR_BATCH_TOO_MANY_OUTPUT_NOTES="batch exceeds maximum output notes" + +# NOTE VALIDATION +# ================================================================================================= + +#! Validates all notes in the batch. +#! +#! This procedure: +#! 1. Collects input notes from all transactions +#! 2. Collects output notes from all transactions +#! 3. Checks for duplicate input notes +#! 4. Checks for duplicate output notes +#! 5. Erases consumed unauthenticated notes +#! 6. Validates counts are within limits +#! +#! Inputs: [] +#! Outputs: [] +pub proc validate_notes + # TODO: Implement note validation + push.0 drop +end diff --git a/crates/miden-protocol/asm/kernels/batch/lib/prologue.masm b/crates/miden-protocol/asm/kernels/batch/lib/prologue.masm new file mode 100644 index 0000000000..1c4f7b0393 --- /dev/null +++ b/crates/miden-protocol/asm/kernels/batch/lib/prologue.masm @@ -0,0 +1,226 @@ +#! Batch Kernel Prologue +#! +#! Handles initialization: unhashing inputs, loading transaction data from advice, +#! and validating basic batch constraints. + +use $kernel::memory +use miden::core::crypto::hashes::rpo256 +use miden::core::word + +# CONSTANTS +# ================================================================================================= + +# Maximum number of transactions allowed in a single batch +const MAX_TRANSACTIONS_PER_BATCH=4 + +# ERRORS +# ================================================================================================= + +const ERR_BATCH_EMPTY="batch contains no transactions" +const ERR_BATCH_TOO_LARGE="batch exceeds maximum transactions per batch" +const ERR_TX_COMMITMENT_MISMATCH="transaction list does not hash to TRANSACTIONS_COMMITMENT" +const ERR_DUPLICATE_TX_ID="duplicate transaction id in batch" + +# PROLOGUE +# ================================================================================================= + +#! Prepares the batch by unhashing inputs and loading data into memory. +#! +#! Inputs: +#! Operand stack: [BLOCK_HASH, TRANSACTIONS_COMMITMENT] +#! Outputs: +#! Operand stack: [] +pub proc prepare_batch + # Store BLOCK_HASH and load block header fields from advice + exec.memory::set_block_hash + # => [TRANSACTIONS_COMMITMENT] + + adv_push.1 exec.memory::set_block_num + # => [TRANSACTIONS_COMMITMENT] + + adv_push.4 exec.memory::set_chain_commitment + # => [TRANSACTIONS_COMMITMENT] + + # Save TRANSACTIONS_COMMITMENT for later verification + exec.memory::set_saved_tx_commitment + # => [] + + # Load and validate transaction count + adv_push.1 + # => [tx_count] + + dup eq.0 assertz.err=ERR_BATCH_EMPTY + dup push.MAX_TRANSACTIONS_PER_BATCH lte assert.err=ERR_BATCH_TOO_LARGE + exec.memory::set_transaction_count + # => [] + + # Load and verify transaction list + exec.load_and_verify_transaction_list + exec.check_duplicate_tx_ids + exec.load_transaction_preimages + # => [] + + # Initialize batch expiration to max u32 (computed from tx expirations later) + push.0xFFFFFFFF exec.memory::set_batch_expiration_block_num + # => [] +end + +# TRANSACTION DATA +# ================================================================================================= + +#! Loads transaction list from advice, stores in memory, and verifies hash. +#! +#! Hash format: [(TX_ID, account_id_prefix, account_id_suffix, 0, 0), ...] per transaction. +#! +#! Inputs: +#! Operand stack: [] +#! Outputs: +#! Operand stack: [] +proc load_and_verify_transaction_list + exec.memory::get_transaction_count push.0 + # => [i, tx_count] + + dup.1 dup.1 gt + # => [should_loop, i, tx_count] + + while.true + # Compute pointers: hash_ptr = HASH_BUFFER + i*8, tx_ptr = TX_DATA + i*48 + dup push.8 mul exec.memory::get_hash_buffer_ptr add + dup.1 exec.memory::get_tx_data_ptr + # => [tx_ptr, hash_ptr, i, tx_count] + + # Load and store TX_ID to both locations + padw adv_loadw + # => [TX_ID, tx_ptr, hash_ptr, i, tx_count] + + dup.4 mem_storew_be + dup.5 mem_storew_be dropw + # => [tx_ptr, hash_ptr, i, tx_count] + + # Load and store account_id (prefix and suffix) + adv_push.1 dup dup.2 add.4 mem_store dup.2 add.4 mem_store + adv_push.1 dup dup.2 add.5 mem_store dup.2 add.5 mem_store + # => [tx_ptr, hash_ptr, i, tx_count] + + # Pad hash buffer with zeros at positions 6-7 + push.0.0 dup.3 add.6 mem_store dup.2 add.7 mem_store + # => [tx_ptr, hash_ptr, i, tx_count] + + # Next iteration + drop drop add.1 + # => [i+1, tx_count] + + dup.1 dup.1 gt + # => [should_loop, i+1, tx_count] + end + + drop + # => [tx_count] + + # Verify: hash(tx_data) == TRANSACTIONS_COMMITMENT + push.8 mul exec.memory::get_hash_buffer_ptr + # => [hash_buffer_end_ptr] + + exec.rpo256::hash_elements + # => [COMPUTED_HASH] + + exec.memory::get_saved_tx_commitment + # => [SAVED_TX_COMMITMENT, COMPUTED_HASH] + + assert_eqw.err=ERR_TX_COMMITMENT_MISMATCH + # => [] +end + +#! Loads transaction preimages from advice into memory. +#! +#! For each transaction: INIT_ACCOUNT_COMMITMENT, FINAL_ACCOUNT_COMMITMENT, +#! INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, expiration_block_num. +#! +#! Inputs: +#! Operand stack: [] +#! Outputs: +#! Operand stack: [] +proc load_transaction_preimages + exec.memory::get_transaction_count push.0 + # => [i, tx_count] + + dup.1 dup.1 gt + # => [should_loop, i, tx_count] + + while.true + dup exec.memory::get_tx_data_ptr + # => [tx_ptr, i, tx_count] + + padw adv_loadw dup.4 add.8 mem_storew_be dropw # INIT_ACCOUNT_COMMITMENT + padw adv_loadw dup.4 add.12 mem_storew_be dropw # FINAL_ACCOUNT_COMMITMENT + padw adv_loadw dup.4 add.16 mem_storew_be dropw # INPUT_NOTES_COMMITMENT + padw adv_loadw dup.4 add.20 mem_storew_be dropw # OUTPUT_NOTES_COMMITMENT + adv_push.1 dup.1 add.6 mem_store # expiration_block_num + # => [tx_ptr, i, tx_count] + + drop add.1 + # => [i+1, tx_count] + + dup.1 dup.1 gt + # => [should_loop, i+1, tx_count] + end + + drop drop + # => [] +end + +# VALIDATION +# ================================================================================================= + +#! Checks for duplicate TX_IDs using O(n²) pairwise comparison. +#! +#! Inputs: +#! Operand stack: [] +#! Outputs: +#! Operand stack: [] +proc check_duplicate_tx_ids + exec.memory::get_transaction_count push.0 + # => [i, tx_count] + + dup.1 push.1 sub dup.1 gt + # => [should_loop, i, tx_count] + + while.true + # Inner loop: compare TX_ID[i] with TX_ID[j] for j in [i+1, tx_count) + dup add.1 + # => [j, i, tx_count] + + dup.2 dup.1 gt + # => [should_loop, j, i, tx_count] + + while.true + # Load TX_ID[i] + dup.1 exec.memory::get_tx_data_ptr + padw movup.4 mem_loadw_be + # => [TX_ID_I, j, i, tx_count] + + # Load TX_ID[j] + dup.4 exec.memory::get_tx_data_ptr + padw movup.4 mem_loadw_be + # => [TX_ID_J, TX_ID_I, j, i, tx_count] + + exec.word::eq assertz.err=ERR_DUPLICATE_TX_ID + # => [j, i, tx_count] + + add.1 + # => [j+1, i, tx_count] + + dup.2 dup.1 gt + # => [should_loop, j+1, i, tx_count] + end + + drop add.1 + # => [i+1, tx_count] + + dup.1 push.1 sub dup.1 gt + # => [should_loop, i+1, tx_count] + end + + drop drop + # => [] +end diff --git a/crates/miden-protocol/asm/kernels/batch/lib/transaction.masm b/crates/miden-protocol/asm/kernels/batch/lib/transaction.masm new file mode 100644 index 0000000000..c84768d088 --- /dev/null +++ b/crates/miden-protocol/asm/kernels/batch/lib/transaction.masm @@ -0,0 +1,75 @@ +#! Batch Kernel Transaction Module +#! +#! This module handles transaction processing: +#! - Validating transaction expiration +#! - Tracking minimum expiration for batch +#! +#! NOTE: Recursive STARK proof verification is NOT implemented yet. +#! Transaction IDs serve as commitments to proven transactions. + +use $kernel::memory + +# ERRORS +# ================================================================================================= + +const ERR_TX_EXPIRED="transaction has expired relative to batch reference block" + +# TRANSACTION PROCESSING +# ================================================================================================= + +#! Processes all transactions in the batch. +#! +#! For each transaction: +#! 1. Validate transaction hasn't expired (expiration > block_num) +#! 2. Update batch_expiration to min(batch_expiration, tx_expiration) +#! +#! NOTE: Recursive STARK verification is NOT implemented yet. +#! Transaction data was already loaded by prologue. +#! +#! Inputs: +#! Operand stack: [] +#! Outputs: +#! Operand stack: [] +pub proc process_transactions + exec.memory::get_transaction_count push.0 + # => [i, tx_count] + + dup.1 dup.1 gt + # => [should_loop, i, tx_count] + + while.true + # Get tx_expiration_block_num from memory (at tx_data_ptr + 6) + dup exec.memory::get_tx_data_ptr push.6 add mem_load + # => [tx_expiration, i, tx_count] + + # Validate: tx_expiration > block_num + dup exec.memory::get_block_num gt assert.err=ERR_TX_EXPIRED + # => [tx_expiration, i, tx_count] + + # Update batch_expiration = min(batch_expiration, tx_expiration) + exec.memory::get_batch_expiration_block_num + # => [batch_expiration, tx_expiration, i, tx_count] + + dup.1 dup.1 lt + # => [tx_expiration < batch_expiration, batch_expiration, tx_expiration, i, tx_count] + + if.true + # tx_expiration is smaller, use it + drop exec.memory::set_batch_expiration_block_num + # => [i, tx_count] + else + # batch_expiration is smaller or equal, keep it + drop drop + # => [i, tx_count] + end + + add.1 + # => [i+1, tx_count] + + dup.1 dup.1 gt + # => [should_loop, i+1, tx_count] + end + + drop drop + # => [] +end diff --git a/crates/miden-protocol/asm/kernels/batch/main.masm b/crates/miden-protocol/asm/kernels/batch/main.masm new file mode 100644 index 0000000000..70d6bbbcfb --- /dev/null +++ b/crates/miden-protocol/asm/kernels/batch/main.masm @@ -0,0 +1,94 @@ +#! Miden Batch Kernel +#! +#! This is the main program for proving a transaction batch. The batch kernel loads +#! transaction data from the advice provider, validates batch-level constraints, and +#! produces output commitments for the block kernel. +#! +#! All of the required data is loaded from the advice provider: +#! - Block header preimage (for unhashing BLOCK_HASH) +#! - Transaction list preimage (for unhashing TRANSACTIONS_COMMITMENT) +#! - Transaction data (account states, notes) +#! - Note data for computing input/output commitments +#! +#! NOTE: Recursive STARK verification of transaction proofs is NOT implemented yet. +#! Currently, transaction validation uses only transaction IDs as commitments. +#! +#! # Inputs +#! +#! Inputs are provided via the operand stack: +#! ```text +#! [BLOCK_HASH, TRANSACTIONS_COMMITMENT] +#! ``` +#! +#! Where: +#! - BLOCK_HASH is the commitment to the reference block header. +#! - TRANSACTIONS_COMMITMENT (BatchId) is a sequential hash of [(TX_ID, account_id), ...]. +#! +#! # Outputs +#! +#! Outputs are left on the operand stack: +#! ```text +#! [INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_SMT_ROOT, batch_expiration_block_num, ...] +#! ``` +#! +#! Where: +#! - INPUT_NOTES_COMMITMENT is a sequential hash of [(nullifier, empty_word_or_note_hash), ...] +#! where empty_word_or_note_hash is hash(note_id, note_metadata) for unauthenticated notes. +#! - OUTPUT_NOTES_SMT_ROOT is the root of the output notes Sparse Merkle Tree. +#! - batch_expiration_block_num is the minimum expiration block across all transactions. + +use $kernel::prologue +use $kernel::transaction +use $kernel::note +use $kernel::account +use $kernel::epilogue + +# MAIN +# ================================================================================================= + +#! Batch kernel main program. +#! +#! Inputs: [BLOCK_HASH, TRANSACTIONS_COMMITMENT] +#! Outputs: [INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_SMT_ROOT, batch_expiration_block_num, ...] +proc main + # PHASE 1: PROLOGUE + # ------------------------------------------------------------------------- + # Unhash block header to get block fields + # Unhash transactions commitment to get [(TX_ID, account_id), ...] + exec.prologue::prepare_batch + + # PHASE 2: PROCESS TRANSACTIONS + # ------------------------------------------------------------------------- + # For each transaction: + # - Load transaction data from advice (tx_id is the commitment) + # - Validate transaction hasn't expired + # - Track minimum expiration for batch expiration + # NOTE: Recursive STARK verification is NOT implemented yet + exec.transaction::process_transactions + + # PHASE 3: VALIDATE CONSTRAINTS + # ------------------------------------------------------------------------- + # Check account state transitions (A->B->C ordering) + exec.account::validate_account_updates + + # Check note constraints (no duplicates, counts within limits) + exec.note::validate_notes + + # PHASE 4: VERIFY TRANSACTION PROOFS + # ------------------------------------------------------------------------- + # TODO: Recursive STARK verification of transaction proofs goes here. + # This is expensive and runs AFTER all cheap validation passes. + # Currently stubbed - using tx_id as commitment without proof verification. + + # PHASE 5: EPILOGUE + # ------------------------------------------------------------------------- + # Compute output commitments + # - INPUT_NOTES_COMMITMENT + # - OUTPUT_NOTES_SMT_ROOT + # - batch_expiration_block_num + exec.epilogue::finalize_batch +end + +begin + exec.main +end diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/link_map.masm b/crates/miden-protocol/asm/kernels/shared/link_map.masm similarity index 98% rename from crates/miden-protocol/asm/kernels/transaction/lib/link_map.masm rename to crates/miden-protocol/asm/kernels/shared/link_map.masm index 3ed748cb27..f9fcd505be 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/link_map.masm +++ b/crates/miden-protocol/asm/kernels/shared/link_map.masm @@ -2,6 +2,13 @@ use miden::core::collections::smt use miden::core::word use $kernel::memory +# TODO: Consider moving LinkMap to miden-stdlib (std::collections) +# To make it reusable outside kernel context, add a `link_map::new(start_ptr, end_ptr)` +# constructor that stores the memory region in metadata instead of using hardcoded +# kernel-specific memory procs. This would allow users to allocate their desired +# memory region (determining max elements) while the stdlib doesn't need to care about that. +# See: https://github.com/0xMiden/miden-base/pull/1428#discussion_r2041192629 + # A link map is a map data structure based on a sorted linked list. # # # Basics & Terminology diff --git a/crates/miden-protocol/build.rs b/crates/miden-protocol/build.rs index 83db0059aa..980ddc8854 100644 --- a/crates/miden-protocol/build.rs +++ b/crates/miden-protocol/build.rs @@ -23,7 +23,9 @@ const ASM_PROTOCOL_DIR: &str = "protocol"; const SHARED_UTILS_DIR: &str = "shared_utils"; const SHARED_MODULES_DIR: &str = "shared_modules"; +const KERNEL_SHARED_MODULES_DIR: &str = "kernels/shared"; const ASM_TX_KERNEL_DIR: &str = "kernels/transaction"; +const ASM_BATCH_KERNEL_DIR: &str = "kernels/batch"; const KERNEL_PROCEDURES_RS_FILE: &str = "src/transaction/kernel/procedures.rs"; const PROTOCOL_LIB_NAMESPACE: &str = "miden::protocol"; @@ -76,6 +78,9 @@ fn main() -> Result<()> { // copy the shared modules to the kernel and protocol library folders copy_shared_modules(&source_dir)?; + // copy the kernel-only shared modules to the kernel library folders + copy_kernel_shared_modules(&source_dir)?; + // set target directory to {OUT_DIR}/assets let target_dir = Path::new(&build_dir).join(ASSETS_DIR); @@ -83,6 +88,9 @@ fn main() -> Result<()> { let mut assembler = compile_tx_kernel(&source_dir.join(ASM_TX_KERNEL_DIR), &target_dir.join("kernels"))?; + // compile batch kernel + compile_batch_kernel(&source_dir.join(ASM_BATCH_KERNEL_DIR), &target_dir.join("kernels"))?; + // compile protocol library let protocol_lib = compile_protocol_lib(&source_dir, &target_dir, assembler.clone())?; assembler.link_dynamic_library(protocol_lib)?; @@ -192,6 +200,43 @@ fn compile_tx_script_main( tx_script_main.write_to_file(masb_file_path).into_diagnostic() } +// COMPILE BATCH KERNEL +// ================================================================================================ + +/// Reads the batch kernel MASM source from the `source_dir`, compiles it, and saves the results +/// to the `target_dir`. +/// +/// `source_dir` is expected to have the following structure: +/// +/// - {source_dir}/main.masm -> defines the executable program of the batch kernel. +/// - {source_dir}/lib -> contains modules used by main.masm. +/// +/// The compiled files are written as follows: +/// +/// - {target_dir}/batch_kernel.masb -> contains the executable compiled from main.masm. +/// +/// NOTE: Unlike the transaction kernel, the batch kernel does not have an api.masm file +/// because it doesn't expose syscall procedures. It's a standalone program. +fn compile_batch_kernel(source_dir: &Path, target_dir: &Path) -> Result<()> { + let shared_utils_path = std::path::Path::new(ASM_DIR).join(SHARED_UTILS_DIR); + let kernel_path = miden_assembly::Path::kernel_path(); + + let mut assembler = build_assembler(None)?; + // add the shared util modules under the ::$kernel::util namespace + assembler.compile_and_statically_link_from_dir(&shared_utils_path, kernel_path)?; + // add the batch kernel lib modules under the ::$kernel namespace + assembler.compile_and_statically_link_from_dir(source_dir.join("lib"), kernel_path)?; + + // assemble the kernel program and write it to the "batch_kernel.masb" file + let main_file_path = source_dir.join("main.masm"); + let kernel_main = assembler.assemble_program(main_file_path)?; + + let masb_file_path = target_dir.join("batch_kernel.masb"); + kernel_main.write_to_file(masb_file_path).into_diagnostic()?; + + Ok(()) +} + /// Generates kernel `procedures.rs` file based on the kernel library fn generate_kernel_proc_hash_file(kernel: KernelLibrary) -> Result<()> { // Because the kernel Rust file will be stored under ./src, this should be a no-op if we can't @@ -315,9 +360,13 @@ fn copy_shared_modules>(source_dir: T) -> Result<()> { for module_path in shared::get_masm_files(shared_modules_dir).unwrap() { let module_name = module_path.file_name().unwrap(); - // copy to kernel lib - let kernel_lib_folder = source_dir.as_ref().join(ASM_TX_KERNEL_DIR).join("lib"); - fs::copy(&module_path, kernel_lib_folder.join(module_name)).into_diagnostic()?; + // copy to transaction kernel lib + let tx_kernel_lib_folder = source_dir.as_ref().join(ASM_TX_KERNEL_DIR).join("lib"); + fs::copy(&module_path, tx_kernel_lib_folder.join(module_name)).into_diagnostic()?; + + // copy to batch kernel lib + let batch_kernel_lib_folder = source_dir.as_ref().join(ASM_BATCH_KERNEL_DIR).join("lib"); + fs::copy(&module_path, batch_kernel_lib_folder.join(module_name)).into_diagnostic()?; // copy to protocol lib let protocol_lib_folder = source_dir.as_ref().join(ASM_PROTOCOL_DIR); @@ -327,6 +376,33 @@ fn copy_shared_modules>(source_dir: T) -> Result<()> { Ok(()) } +/// Copies the content of the build `kernel_shared_modules` folder to the kernel `lib` folders only. +/// These modules depend on kernel-specific namespaces (like `$kernel::memory`) and cannot be used +/// in the protocol library. +/// +/// This is done to make it possible to import the modules in the `kernel_shared_modules` folder +/// directly in kernels, i.e. "use $kernel::link_map". +fn copy_kernel_shared_modules>(source_dir: T) -> Result<()> { + // source is expected to be an `OUT_DIR/asm` folder + let kernel_shared_modules_dir = source_dir.as_ref().join(KERNEL_SHARED_MODULES_DIR); + + for module_path in shared::get_masm_files(kernel_shared_modules_dir).unwrap() { + let module_name = module_path.file_name().unwrap(); + + // copy to transaction kernel lib + let tx_kernel_lib_folder = source_dir.as_ref().join(ASM_TX_KERNEL_DIR).join("lib"); + fs::copy(&module_path, tx_kernel_lib_folder.join(module_name)).into_diagnostic()?; + + // copy to batch kernel lib + let batch_kernel_lib_folder = source_dir.as_ref().join(ASM_BATCH_KERNEL_DIR).join("lib"); + fs::copy(&module_path, batch_kernel_lib_folder.join(module_name)).into_diagnostic()?; + + // NOTE: NOT copying to protocol lib - these modules depend on $kernel::memory + } + + Ok(()) +} + // ERROR CONSTANTS FILE GENERATION // ================================================================================================ diff --git a/crates/miden-protocol/src/batch/kernel/advice_inputs.rs b/crates/miden-protocol/src/batch/kernel/advice_inputs.rs new file mode 100644 index 0000000000..8f7f3e7af7 --- /dev/null +++ b/crates/miden-protocol/src/batch/kernel/advice_inputs.rs @@ -0,0 +1,100 @@ +//! Batch Kernel Advice Inputs +//! +//! This module is responsible for preparing the advice inputs for the batch kernel. +//! The advice inputs contain all the preimages needed for unhashing, as well as +//! the transaction proofs for recursive verification. + +use alloc::sync::Arc; +use alloc::vec::Vec; + +use crate::block::BlockHeader; +use crate::transaction::ProvenTransaction; +use crate::vm::AdviceInputs; +use crate::{Felt, ZERO}; + +// BATCH ADVICE INPUTS +// ================================================================================================ + +/// Holds the advice inputs required by the batch kernel. +/// +/// The advice inputs include: +/// - Block header preimage (for unhashing BLOCK_HASH) +/// - Transaction list preimage (for unhashing TRANSACTIONS_COMMITMENT) +/// - Transaction ID preimages (for unhashing each TX_ID) +/// - Transaction proofs (for recursive verification) +/// - Input/output note data +#[derive(Debug, Clone)] +pub struct BatchAdviceInputs { + inner: AdviceInputs, +} + +impl BatchAdviceInputs { + /// Creates a new [BatchAdviceInputs] from a block header and list of transactions. + /// + /// This method extracts all the data needed by the batch kernel and organizes it + /// into the advice stack and advice map. + pub fn new(block_header: &BlockHeader, transactions: &[Arc]) -> Self { + let mut advice_stack: Vec = Vec::new(); + let advice_map = alloc::collections::BTreeMap::new(); + + // Build advice stack in the order MASM will pop it + // (element 0 = top of advice stack = first popped) + + // Block header data + advice_stack.push(Felt::from(block_header.block_num())); + advice_stack.extend(block_header.chain_commitment()); + + // Transaction count + advice_stack.push(Felt::new(transactions.len() as u64)); + + // Transaction list data (TX_ID, account_id for each tx) + for tx in transactions { + advice_stack.extend(tx.id().as_elements()); + advice_stack.push(tx.account_id().prefix().as_felt()); + advice_stack.push(tx.account_id().suffix()); + } + + // Transaction preimages + for tx in transactions { + let account_update = tx.account_update(); + advice_stack.extend(account_update.initial_state_commitment()); + advice_stack.extend(account_update.final_state_commitment()); + advice_stack.extend(tx.input_notes().commitment()); + advice_stack.extend(tx.output_notes().commitment()); + advice_stack.push(Felt::from(tx.expiration_block_num())); + } + + // Input notes data + for tx in transactions { + let input_notes = tx.input_notes(); + advice_stack.push(Felt::new(input_notes.num_notes() as u64)); + + for note in input_notes.iter() { + advice_stack.extend(note.nullifier().as_elements()); + // TODO: For unauthenticated notes, use hash(note_id, note_metadata) instead of + // zeros + advice_stack.extend([ZERO; 4]); + } + } + + Self { + inner: AdviceInputs::default().with_stack(advice_stack).with_map(advice_map), + } + } + + /// Returns the inner [AdviceInputs]. + pub fn into_inner(self) -> AdviceInputs { + self.inner + } + + /// Returns a reference to the inner [AdviceInputs]. + pub fn inner(&self) -> &AdviceInputs { + &self.inner + } +} + +impl From for AdviceInputs { + fn from(inputs: BatchAdviceInputs) -> Self { + inputs.inner + } +} diff --git a/crates/miden-protocol/src/batch/kernel/mod.rs b/crates/miden-protocol/src/batch/kernel/mod.rs new file mode 100644 index 0000000000..e1b7782b8d --- /dev/null +++ b/crates/miden-protocol/src/batch/kernel/mod.rs @@ -0,0 +1,170 @@ +//! Batch Kernel +//! +//! This module provides the Rust wrapper for the batch kernel MASM program. +//! The batch kernel proves the validity of a batch of already-proven transactions. + +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use crate::batch::BatchId; +use crate::block::BlockNumber; +use crate::utils::serde::Deserializable; +use crate::utils::sync::LazyLock; +use crate::vm::{Program, ProgramInfo, StackInputs, StackOutputs}; +use crate::{Felt, Word}; + +mod advice_inputs; +pub use advice_inputs::BatchAdviceInputs; + +// CONSTANTS +// ================================================================================================ + +// Initialize the batch kernel main program only once +static BATCH_KERNEL_MAIN: LazyLock = LazyLock::new(|| { + let kernel_main_bytes = + include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/batch_kernel.masb")); + Program::read_from_bytes(kernel_main_bytes).expect("failed to deserialize batch kernel runtime") +}); + +// BATCH KERNEL ERROR +// ================================================================================================ + +/// Error type for batch kernel operations. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BatchKernelError { + /// Failed to parse output stack. + InvalidOutputStack(String), +} + +impl core::fmt::Display for BatchKernelError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + BatchKernelError::InvalidOutputStack(msg) => { + write!(f, "invalid batch kernel output stack: {}", msg) + }, + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for BatchKernelError {} + +// BATCH KERNEL +// ================================================================================================ + +/// Batch kernel for proving transaction batches. +/// +/// The batch kernel takes a list of proven transactions and produces a single proof +/// that attests to the validity of the entire batch. +pub struct BatchKernel; + +impl BatchKernel { + // KERNEL SOURCE CODE + // -------------------------------------------------------------------------------------------- + + /// Returns an AST of the batch kernel executable program. + /// + /// # Panics + /// Panics if the batch kernel source is not well-formed. + pub fn main() -> Program { + BATCH_KERNEL_MAIN.clone() + } + + /// Returns [ProgramInfo] for the batch kernel executable program. + pub fn program_info() -> ProgramInfo { + Self::main().into() + } + + // INPUT/OUTPUT STACK + // -------------------------------------------------------------------------------------------- + + /// Builds the input stack for the batch kernel. + /// + /// The input stack contains: + /// - `BLOCK_HASH`: The reference block hash (commitment to block header) + /// - `TRANSACTIONS_COMMITMENT` (BatchId): Sequential hash of [(TX_ID, account_id), ...] + /// + /// Stack layout (top to bottom): + /// ```text + /// [BLOCK_HASH, TRANSACTIONS_COMMITMENT] + /// ``` + pub fn build_input_stack(block_hash: Word, batch_id: BatchId) -> StackInputs { + let mut inputs: Vec = Vec::with_capacity(16); + inputs.extend(block_hash); + // Reverse BatchId to match MASM rpo256::hash_elements output order + inputs.extend(batch_id.as_elements().iter().rev()); + // Pad to 16 elements (required for correct stack positioning) + inputs.resize(16, Felt::from(0_u32)); + + StackInputs::new(inputs) + .map_err(|e| e.to_string()) + .expect("Invalid stack input") + } + + /// Parses the output stack from batch kernel execution. + /// + /// Output stack layout: + /// ```text + /// [INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_SMT_ROOT, batch_expiration_block_num, ...] + /// ``` + /// + /// Returns: + /// - `input_notes_commitment`: Sequential hash of [(nullifier, empty_word_or_note_hash), ...] + /// - `output_notes_smt_root`: Root of the output notes Sparse Merkle Tree + /// - `batch_expiration_block_num`: Minimum expiration block across all transactions + pub fn parse_output_stack( + outputs: &StackOutputs, + ) -> Result { + // Output stack layout: + // [INPUT_NOTES_COMMITMENT (0-3), OUTPUT_NOTES_SMT_ROOT (4-7), batch_expiration (8), ...] + + let input_notes_commitment = outputs.get_stack_word_be(0).ok_or_else(|| { + BatchKernelError::InvalidOutputStack( + "input_notes_commitment (first word) missing".to_string(), + ) + })?; + + let output_notes_smt_root = outputs.get_stack_word_be(4).ok_or_else(|| { + BatchKernelError::InvalidOutputStack( + "output_notes_smt_root (second word) missing".to_string(), + ) + })?; + + let batch_expiration_felt = outputs.get_stack_item(8).ok_or_else(|| { + BatchKernelError::InvalidOutputStack( + "batch_expiration_block_num (element at index 8) missing".to_string(), + ) + })?; + + let batch_expiration_block_num: BlockNumber = u32::try_from(batch_expiration_felt.as_int()) + .map_err(|_| { + BatchKernelError::InvalidOutputStack( + "batch expiration block number should be smaller than u32::MAX".to_string(), + ) + })? + .into(); + + Ok(BatchKernelOutputs { + input_notes_commitment, + output_notes_smt_root, + batch_expiration_block_num, + }) + } +} + +// BATCH KERNEL OUTPUTS +// ================================================================================================ + +/// Outputs produced by the batch kernel. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BatchKernelOutputs { + /// Sequential hash of [(nullifier, empty_word_or_note_hash), ...]. + /// For unauthenticated notes, empty_word_or_note_hash is hash(note_id, note_metadata). + pub input_notes_commitment: Word, + + /// Root of the output notes Sparse Merkle Tree. + pub output_notes_smt_root: Word, + + /// Minimum expiration block across all transactions in the batch. + pub batch_expiration_block_num: BlockNumber, +} diff --git a/crates/miden-protocol/src/batch/mod.rs b/crates/miden-protocol/src/batch/mod.rs index 1cef432dd3..69b1e8e295 100644 --- a/crates/miden-protocol/src/batch/mod.rs +++ b/crates/miden-protocol/src/batch/mod.rs @@ -18,3 +18,6 @@ pub use ordered_batches::OrderedBatches; mod input_output_note_tracker; pub(crate) use input_output_note_tracker::InputOutputNoteTracker; + +pub mod kernel; +pub use kernel::{BatchAdviceInputs, BatchKernel, BatchKernelError, BatchKernelOutputs}; diff --git a/crates/miden-protocol/src/batch/proven_batch.rs b/crates/miden-protocol/src/batch/proven_batch.rs index 97075a8736..7768b817b7 100644 --- a/crates/miden-protocol/src/batch/proven_batch.rs +++ b/crates/miden-protocol/src/batch/proven_batch.rs @@ -2,6 +2,7 @@ use alloc::collections::BTreeMap; use alloc::string::ToString; use alloc::vec::Vec; +use crate::Word; use crate::account::AccountId; use crate::batch::{BatchAccountUpdate, BatchId}; use crate::block::BlockNumber; @@ -9,11 +10,15 @@ use crate::errors::ProvenBatchError; use crate::note::Nullifier; use crate::transaction::{InputNoteCommitment, InputNotes, OrderedTransactionHeaders, OutputNote}; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; -use crate::{MIN_PROOF_SECURITY_LEVEL, Word}; +use crate::vm::ExecutionProof; /// A transaction batch with an execution proof. -/// Currently, there is no proof attached. Future versions will extend this structure to include -/// a proof artifact once recursive proving is implemented. +/// +/// The proof attests to the correct execution of the batch kernel, which validates: +/// - Account state transitions are correctly ordered and merged +/// - Input notes are valid (no duplicates, proper authentication) +/// - Output notes form a valid SMT +/// - Batch expiration is computed correctly #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProvenBatch { id: BatchId, @@ -24,6 +29,7 @@ pub struct ProvenBatch { output_notes: Vec, batch_expiration_block_num: BlockNumber, transactions: OrderedTransactionHeaders, + proof: ExecutionProof, } impl ProvenBatch { @@ -36,6 +42,7 @@ impl ProvenBatch { /// /// Returns an error if the batch expiration block number is not greater than the reference /// block number. + #[allow(clippy::too_many_arguments)] pub fn new( id: BatchId, reference_block_commitment: Word, @@ -45,6 +52,7 @@ impl ProvenBatch { output_notes: Vec, batch_expiration_block_num: BlockNumber, transactions: OrderedTransactionHeaders, + proof: ExecutionProof, ) -> Result { // Check that the batch expiration block number is greater than the reference block number. if batch_expiration_block_num <= reference_block_num { @@ -63,6 +71,7 @@ impl ProvenBatch { output_notes, batch_expiration_block_num, transactions, + proof, }) } @@ -89,6 +98,11 @@ impl ProvenBatch { self.batch_expiration_block_num } + /// Returns the execution proof for this batch. + pub fn proof(&self) -> &ExecutionProof { + &self.proof + } + /// Returns an iterator over the IDs of all accounts updated in this batch. pub fn updated_accounts(&self) -> impl Iterator + use<'_> { self.account_updates.keys().copied() @@ -96,7 +110,7 @@ impl ProvenBatch { /// Returns the proof security level of the batch. pub fn proof_security_level(&self) -> u32 { - MIN_PROOF_SECURITY_LEVEL + self.proof.security_level() } /// Returns the map of account IDs mapped to their [`BatchAccountUpdate`]s. @@ -157,6 +171,7 @@ impl Serializable for ProvenBatch { self.output_notes.write_into(target); self.batch_expiration_block_num.write_into(target); self.transactions.write_into(target); + self.proof.write_into(target); } } @@ -170,6 +185,7 @@ impl Deserializable for ProvenBatch { let output_notes = Vec::::read_from(source)?; let batch_expiration_block_num = BlockNumber::read_from(source)?; let transactions = OrderedTransactionHeaders::read_from(source)?; + let proof = ExecutionProof::read_from(source)?; Self::new( id, @@ -180,6 +196,7 @@ impl Deserializable for ProvenBatch { output_notes, batch_expiration_block_num, transactions, + proof, ) .map_err(|e| DeserializationError::UnknownError(e.to_string())) } diff --git a/crates/miden-testing/src/kernel_tests/batch/mod.rs b/crates/miden-testing/src/kernel_tests/batch/mod.rs index b7dcf5b03d..17b01e724a 100644 --- a/crates/miden-testing/src/kernel_tests/batch/mod.rs +++ b/crates/miden-testing/src/kernel_tests/batch/mod.rs @@ -1,2 +1,4 @@ mod proposed_batch; mod proven_tx_builder; +#[cfg(test)] +mod test_prologue; diff --git a/crates/miden-testing/src/kernel_tests/batch/test_prologue.rs b/crates/miden-testing/src/kernel_tests/batch/test_prologue.rs new file mode 100644 index 0000000000..bfe2baf896 --- /dev/null +++ b/crates/miden-testing/src/kernel_tests/batch/test_prologue.rs @@ -0,0 +1,166 @@ +//! Tests for the batch kernel prologue. + +use alloc::string::ToString; +use alloc::sync::Arc; +use alloc::vec::Vec; + +use miden_processor::DefaultHost; +use miden_processor::fast::FastProcessor; +use miden_protocol::account::{Account, AccountStorageMode}; +use miden_protocol::batch::BatchId; +use miden_protocol::batch::kernel::{BatchAdviceInputs, BatchKernel}; +use miden_protocol::block::BlockNumber; +use miden_protocol::transaction::ProvenTransaction; +use miden_protocol::{CoreLibrary, Word}; +use miden_standards::testing::account_component::MockAccountComponent; +use rand::Rng; + +use super::proven_tx_builder::MockProvenTxBuilder; +use crate::{AccountState, Auth, MockChain, MockChainBuilder}; + +fn generate_account(chain: &mut MockChainBuilder) -> Account { + let account_builder = Account::builder(rand::rng().random()) + .storage_mode(AccountStorageMode::Private) + .with_component(MockAccountComponent::with_empty_slots()); + chain + .add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists) + .expect("failed to add pending account from builder") +} + +/// Tests that the batch kernel prologue correctly loads transaction data from advice +/// and the epilogue produces the expected output format. +#[tokio::test] +async fn test_batch_prologue_basic() -> anyhow::Result<()> { + // Set up mock chain with accounts + let mut builder = MockChain::builder(); + let account1 = generate_account(&mut builder); + let account2 = generate_account(&mut builder); + let mut chain = builder.build()?; + chain.prove_next_block()?; + let block_header = chain.block_header(1); + + // Create mock transactions + let tx1 = + MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.commitment()) + .ref_block_commitment(block_header.commitment()) + .expiration_block_num(BlockNumber::from(1000u32)) + .build()?; + + let tx2 = + MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.commitment()) + .ref_block_commitment(block_header.commitment()) + .expiration_block_num(BlockNumber::from(500u32)) + .build()?; + + let transactions: Vec> = vec![Arc::new(tx1), Arc::new(tx2)]; + + // Build inputs + let advice_inputs = BatchAdviceInputs::new(&block_header, &transactions); + let batch_id = BatchId::from_transactions(transactions.iter().map(|t| t.as_ref())); + let stack_inputs = BatchKernel::build_input_stack(block_header.commitment(), batch_id); + + // Execute the batch kernel + let program = BatchKernel::main(); + let mut host = DefaultHost::default(); + + // Load the CoreLibrary MAST forest + let core_lib = CoreLibrary::default(); + host.load_library(core_lib.mast_forest()).expect("failed to load CoreLibrary"); + + let processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs.into()); + let output = processor.execute(&program, &mut host).await?; + + // Parse output and verify basic structure + let parsed = BatchKernel::parse_output_stack(&output.stack)?; + + // Verify batch_expiration is min(tx1.expiration=1000, tx2.expiration=500) = 500 + assert_eq!( + parsed.batch_expiration_block_num, + BlockNumber::from(500u32), + "batch_expiration should be min of transaction expirations" + ); + + // TODO: Once note processing is implemented, verify: + // - input_notes_commitment is correct + // - output_notes_smt_root is correct + + Ok(()) +} + +/// Tests that the batch kernel rejects batches with duplicate transaction IDs. +#[tokio::test] +async fn test_batch_prologue_rejects_duplicate_tx_ids() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account = generate_account(&mut builder); + let mut chain = builder.build()?; + chain.prove_next_block()?; + let block_header = chain.block_header(1); + + // Create two identical transactions (same TX_ID) + let tx = MockProvenTxBuilder::with_account(account.id(), Word::empty(), account.commitment()) + .ref_block_commitment(block_header.commitment()) + .expiration_block_num(BlockNumber::from(1000u32)) + .build()?; + + // Duplicate the same transaction + let transactions: Vec> = vec![Arc::new(tx.clone()), Arc::new(tx)]; + + let advice_inputs = BatchAdviceInputs::new(&block_header, &transactions); + let batch_id = BatchId::from_transactions(transactions.iter().map(|t| t.as_ref())); + let stack_inputs = BatchKernel::build_input_stack(block_header.commitment(), batch_id); + + let program = BatchKernel::main(); + let mut host = DefaultHost::default(); + let core_lib = CoreLibrary::default(); + host.load_library(core_lib.mast_forest()).expect("failed to load CoreLibrary"); + + let processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs.into()); + let result = processor.execute(&program, &mut host).await; + + // Should fail with duplicate TX_ID error + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("duplicate transaction id"), + "expected duplicate TX_ID error, got: {err}" + ); + + Ok(()) +} + +/// Tests that the batch kernel rejects expired transactions. +#[tokio::test] +async fn test_batch_prologue_rejects_expired_transaction() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account = generate_account(&mut builder); + let mut chain = builder.build()?; + chain.prove_next_block()?; + let block_header = chain.block_header(1); + + // Create transaction that expires at block 1 (same as reference block) + let tx = MockProvenTxBuilder::with_account(account.id(), Word::empty(), account.commitment()) + .ref_block_commitment(block_header.commitment()) + .expiration_block_num(BlockNumber::from(1u32)) + .build()?; + + let transactions: Vec> = vec![Arc::new(tx)]; + + let advice_inputs = BatchAdviceInputs::new(&block_header, &transactions); + let batch_id = BatchId::from_transactions(transactions.iter().map(|t| t.as_ref())); + let stack_inputs = BatchKernel::build_input_stack(block_header.commitment(), batch_id); + + let program = BatchKernel::main(); + let mut host = DefaultHost::default(); + let core_lib = CoreLibrary::default(); + host.load_library(core_lib.mast_forest()).expect("failed to load CoreLibrary"); + + let processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs.into()); + let result = processor.execute(&program, &mut host).await; + + // Should fail with expired transaction error + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("expired"), "expected expired transaction error, got: {err}"); + + Ok(()) +} diff --git a/crates/miden-tx-batch-prover/Cargo.toml b/crates/miden-tx-batch-prover/Cargo.toml index d664da1d37..71d1f49c49 100644 --- a/crates/miden-tx-batch-prover/Cargo.toml +++ b/crates/miden-tx-batch-prover/Cargo.toml @@ -18,8 +18,12 @@ bench = false [features] default = ["std"] std = ["miden-protocol/std", "miden-tx/std"] -testing = [] +# miden-air/testing provides ExecutionProof::new_dummy() +testing = ["miden-air/testing", "miden-protocol/testing"] [dependencies] miden-protocol = { workspace = true } miden-tx = { workspace = true } + +# Only needed for ExecutionProof::new_dummy() in testing mode +miden-air = { optional = true, workspace = true } diff --git a/crates/miden-tx-batch-prover/src/local_batch_prover.rs b/crates/miden-tx-batch-prover/src/local_batch_prover.rs index 4e7ccfffc7..b9e0343131 100644 --- a/crates/miden-tx-batch-prover/src/local_batch_prover.rs +++ b/crates/miden-tx-batch-prover/src/local_batch_prover.rs @@ -2,6 +2,7 @@ use alloc::boxed::Box; use miden_protocol::batch::{ProposedBatch, ProvenBatch}; use miden_protocol::errors::ProvenBatchError; +use miden_protocol::vm::ExecutionProof; use miden_tx::TransactionVerifier; // LOCAL BATCH PROVER @@ -30,7 +31,11 @@ impl LocalBatchProver { /// /// Returns an error if: /// - a proof of any transaction in the batch fails to verify. - pub fn prove(&self, proposed_batch: ProposedBatch) -> Result { + pub fn prove( + &self, + proposed_batch: ProposedBatch, + proof: ExecutionProof, + ) -> Result { let verifier = TransactionVerifier::new(self.proof_security_level); for tx in proposed_batch.transactions() { @@ -42,25 +47,28 @@ impl LocalBatchProver { })?; } - self.prove_inner(proposed_batch) + self.build_proven_batch(proposed_batch, proof) } /// Proves the provided [`ProposedBatch`] into a [`ProvenBatch`], **without verifying batches /// and proving the block**. /// - /// This is exposed for testing purposes. + /// This is exposed for testing purposes. Uses a dummy proof. #[cfg(any(feature = "testing", test))] pub fn prove_dummy( &self, proposed_batch: ProposedBatch, ) -> Result { - self.prove_inner(proposed_batch) + let proof = miden_air::ExecutionProof::new_dummy(); + self.build_proven_batch(proposed_batch, proof) } - /// Converts a proposed batch into a proven batch. - /// - /// For now, this doesn't do anything interesting. - fn prove_inner(&self, proposed_batch: ProposedBatch) -> Result { + /// Converts a proposed batch into a proven batch with the given proof. + fn build_proven_batch( + &self, + proposed_batch: ProposedBatch, + proof: ExecutionProof, + ) -> Result { let tx_headers = proposed_batch.transaction_headers(); let ( _transactions, @@ -83,6 +91,7 @@ impl LocalBatchProver { output_notes, batch_expiration_block_num, tx_headers, + proof, ) } }