From 94c499314d0d429dd7f3d606ce78e19431558d44 Mon Sep 17 00:00:00 2001 From: krushimir <189111540+krushimir@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:58:52 +0100 Subject: [PATCH 1/3] feat(batch-kernel): add batch kernel MASM scaffolding and prologue --- .../asm/kernels/batch/lib/account.masm | 36 +++ .../asm/kernels/batch/lib/epilogue.masm | 23 ++ .../asm/kernels/batch/lib/memory.masm | 283 ++++++++++++++++++ .../asm/kernels/batch/lib/note.masm | 45 +++ .../asm/kernels/batch/lib/prologue.masm | 194 ++++++++++++ .../asm/kernels/batch/lib/transaction.masm | 44 +++ .../asm/kernels/batch/main.masm | 88 ++++++ .../{transaction/lib => shared}/link_map.masm | 7 + crates/miden-protocol/build.rs | 82 ++++- .../src/batch/kernel/advice_inputs.rs | 105 +++++++ crates/miden-protocol/src/batch/kernel/mod.rs | 169 +++++++++++ crates/miden-protocol/src/batch/mod.rs | 3 + .../miden-protocol/src/batch/proven_batch.rs | 24 +- .../src/kernel_tests/batch/mod.rs | 2 + .../src/kernel_tests/batch/test_prologue.rs | 94 ++++++ .../src/local_batch_prover.rs | 25 +- 16 files changed, 1209 insertions(+), 15 deletions(-) create mode 100644 crates/miden-protocol/asm/kernels/batch/lib/account.masm create mode 100644 crates/miden-protocol/asm/kernels/batch/lib/epilogue.masm create mode 100644 crates/miden-protocol/asm/kernels/batch/lib/memory.masm create mode 100644 crates/miden-protocol/asm/kernels/batch/lib/note.masm create mode 100644 crates/miden-protocol/asm/kernels/batch/lib/prologue.masm create mode 100644 crates/miden-protocol/asm/kernels/batch/lib/transaction.masm create mode 100644 crates/miden-protocol/asm/kernels/batch/main.masm rename crates/miden-protocol/asm/kernels/{transaction/lib => shared}/link_map.masm (98%) create mode 100644 crates/miden-protocol/src/batch/kernel/advice_inputs.rs create mode 100644 crates/miden-protocol/src/batch/kernel/mod.rs create mode 100644 crates/miden-testing/src/kernel_tests/batch/test_prologue.rs 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..4b89188d6f --- /dev/null +++ b/crates/miden-protocol/asm/kernels/batch/lib/epilogue.masm @@ -0,0 +1,23 @@ +#! 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: +#! [INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_SMT_ROOT, batch_expiration_block_num, ...] +#! +#! Inputs: [] +#! Outputs: [INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_SMT_ROOT, batch_expiration_block_num, ...] +pub proc finalize_batch + exec.memory::get_batch_expiration_block_num + padw # TODO: compute OUTPUT_NOTES_SMT_ROOT + padw # TODO: compute INPUT_NOTES_COMMITMENT + exec.sys::truncate_stack +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..588f9c8c93 --- /dev/null +++ b/crates/miden-protocol/asm/kernels/batch/lib/prologue.masm @@ -0,0 +1,194 @@ +#! Batch Kernel Prologue +#! +#! This module handles the initialization phase of the batch kernel: +#! - Unhashing the block hash to extract block header fields +#! - Unhashing the transactions commitment to get the list of (TX_ID, account_id) pairs +#! - Validating that the batch is not empty +#! - Setting up memory with initial data + +use $kernel::memory +use miden::core::crypto::hashes::rpo256 + +# CONSTANTS +# ================================================================================================= + +# Maximum number of transactions per batch +const MAX_TRANSACTIONS_PER_BATCH=4 + +# Error constants +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" + +# PROLOGUE +# ================================================================================================= + +#! Prepares the batch by unhashing inputs and loading data into memory. +#! +#! This procedure: +#! 1. Takes BLOCK_HASH and TRANSACTIONS_COMMITMENT from the stack +#! 2. Unhashes BLOCK_HASH to get block header fields (from advice) +#! 3. Unhashes TRANSACTIONS_COMMITMENT to get [(TX_ID, account_id), ...] (from advice) +#! 4. Validates batch is not empty +#! 5. Verifies transaction list hashes to TRANSACTIONS_COMMITMENT +#! 6. Stores all data in memory for subsequent phases +#! +#! Inputs: [BLOCK_HASH, TRANSACTIONS_COMMITMENT] +#! Outputs: [] +#! +#! Errors: +#! - ERR_BATCH_EMPTY: batch contains no transactions +#! - ERR_BATCH_TOO_LARGE: batch exceeds MAX_TRANSACTIONS_PER_BATCH +#! - ERR_TX_COMMITMENT_MISMATCH: transaction list does not hash to TRANSACTIONS_COMMITMENT +pub proc prepare_batch + # ========================================================================= + # PHASE 1: Process BLOCK_HASH + # ========================================================================= + # Initial stack: [BLOCK_HASH, TRANSACTIONS_COMMITMENT] + + # Store BLOCK_HASH in memory (needed for MMR authentication later) + exec.memory::set_block_hash + # Stack: [TRANSACTIONS_COMMITMENT, ...] + + # Read block_num from advice and store + adv_push.1 + exec.memory::set_block_num + # Stack: [TRANSACTIONS_COMMITMENT, ...] + + # Read CHAIN_COMMITMENT from advice and store + adv_push.4 + exec.memory::set_chain_commitment + # Stack: [TRANSACTIONS_COMMITMENT, ...] + + + # Save TRANSACTIONS_COMMITMENT for later verification + exec.memory::set_saved_tx_commitment + # Stack: [] + + # Load and validate transaction count + adv_push.1 + # Stack: [tx_count] + + # Validate: batch not empty + dup eq.0 assertz.err=ERR_BATCH_EMPTY + + # Validate: batch not too large + dup push.MAX_TRANSACTIONS_PER_BATCH lte assert.err=ERR_BATCH_TOO_LARGE + + # Store transaction count + exec.memory::set_transaction_count + # Stack: [] + + # Load transaction list from advice + exec.load_and_verify_transaction_list + + # Load transaction preimages from advice + exec.load_transaction_preimages + + # Initialize batch expiration to max u32 (minimum across txs computed later) + push.0xFFFFFFFF + exec.memory::set_batch_expiration_block_num +end + +#! Loads the transaction list from advice, verifies it hashes to TRANSACTIONS_COMMITMENT, +#! and stores the data in memory. +#! +#! The hash format for BatchId is: [(TX_ID, account_id_prefix, account_id_suffix, 0, 0), ...] +#! Each transaction contributes 8 felts (2 words). +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Errors: +#! - ERR_TX_COMMITMENT_MISMATCH if hash doesn't match +proc load_and_verify_transaction_list + exec.memory::get_transaction_count + push.0 + # => [i, tx_count] + + while.true + dup.1 dup.1 gt + # => [tx_count > i, i, tx_count] + + if.true + # Compute pointers for this transaction + 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 TX_ID and store in both structured storage and hash buffer + padw adv_loadw + dup.4 mem_storew_be + dup.5 mem_storew_be dropw + # => [tx_ptr, hash_ptr, i, tx_count] + + # Load and store account_id_prefix + adv_push.1 + dup dup.2 add.4 mem_store + dup.2 add.4 mem_store + # => [tx_ptr, hash_ptr, i, tx_count] + + # Load and store account_id_suffix + 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] + + # Increment loop counter + drop drop add.1 + push.1 + else + push.0 + end + end + + drop + # => [tx_count] + + # TODO: Verify hash of transaction data matches TRANSACTIONS_COMMITMENT + drop +end + +#! Loads transaction preimages from advice into memory. +#! For each tx: INIT_STATE, FINAL_STATE, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, expiration +#! +#! Inputs: [] +#! Outputs: [] +proc load_transaction_preimages + exec.memory::get_transaction_count + push.0 + # => [i, tx_count] + + while.true + dup.1 dup.1 gt + # => [tx_count > i, i, tx_count] + + if.true + dup exec.memory::get_tx_data_ptr + # => [tx_ptr, i, tx_count] + + # Load account state commitments + 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 + + # Load expiration_block_num + adv_push.1 dup.1 add.6 mem_store + # => [tx_ptr, i, tx_count] + + drop add.1 + push.1 + else + push.0 + end + 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..22f866a832 --- /dev/null +++ b/crates/miden-protocol/asm/kernels/batch/lib/transaction.masm @@ -0,0 +1,44 @@ +#! Batch Kernel Transaction Module +#! +#! This module handles transaction processing: +#! - Loading transaction data from advice provider +#! - 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 + +# CONSTANTS +# ================================================================================================= + +# Error: transaction has expired +const ERR_BATCH_TX_EXPIRED="transaction has expired relative to batch reference block" + +# TRANSACTION PROCESSING +# ================================================================================================= + +#! Processes all transactions in the batch. +#! +#! For each transaction: +#! 1. Load transaction data from advice (tx_id is the commitment) +#! 2. Validate transaction hasn't expired +#! 3. Track minimum expiration for batch expiration +#! +#! NOTE: Recursive STARK verification is NOT implemented yet. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Errors: +#! - ERR_BATCH_TX_EXPIRED: transaction expired before batch reference block +pub proc process_transactions + # TODO: Implement transaction processing loop + # 1. Get transaction count from memory + # 2. Loop over each transaction + # 3. For each: load data, validate expiration, track min expiration + + # For now, this is a placeholder that does nothing + push.0 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..c205fa3e0d --- /dev/null +++ b/crates/miden-protocol/asm/kernels/batch/main.masm @@ -0,0 +1,88 @@ +#! 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: 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..1544d87cc9 --- /dev/null +++ b/crates/miden-protocol/src/batch/kernel/advice_inputs.rs @@ -0,0 +1,105 @@ +//! 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..e32043aebb --- /dev/null +++ b/crates/miden-protocol/src/batch/kernel/mod.rs @@ -0,0 +1,169 @@ +//! 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(8); + // BatchId (TRANSACTIONS_COMMITMENT) - will be below block_hash + inputs.extend(batch_id.as_elements()); + // Block hash - will be at top of stack + inputs.extend(block_hash); + // VM auto-pads to 16 elements with zeros + + 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..98d8e8d628 100644 --- a/crates/miden-protocol/src/batch/proven_batch.rs +++ b/crates/miden-protocol/src/batch/proven_batch.rs @@ -9,11 +9,16 @@ 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; +use crate::Word; /// 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 { @@ -45,6 +51,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 +70,7 @@ impl ProvenBatch { output_notes, batch_expiration_block_num, transactions, + proof, }) } @@ -89,6 +97,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 +109,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 +170,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 +184,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 +195,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..6c6528c6bc --- /dev/null +++ b/crates/miden-testing/src/kernel_tests/batch/test_prologue.rs @@ -0,0 +1,94 @@ +//! Tests for the batch kernel prologue. + +use alloc::sync::Arc; +use alloc::vec::Vec; + +use miden_processor::DefaultHost; +use miden_processor::fast::FastProcessor; +use rand::Rng; +use miden_protocol::account::{Account, AccountStorageMode}; +use miden_protocol::batch::kernel::{BatchAdviceInputs, BatchKernel}; +use miden_protocol::batch::BatchId; +use miden_protocol::block::BlockNumber; +use miden_protocol::transaction::ProvenTransaction; +use miden_protocol::{CoreLibrary, Word}; +use miden_standards::testing::account_component::MockAccountComponent; + +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 the placeholder max value + assert_eq!( + parsed.batch_expiration_block_num, + BlockNumber::from(0xFFFFFFFF_u32), + "batch_expiration should be max u32 (placeholder)" + ); + + // TODO: Once note processing is implemented, verify: + // - input_notes_commitment is correct + // - output_notes_smt_root is correct + // - batch_expiration equals min(tx1.expiration, tx2.expiration) = 500 + + Ok(()) +} 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, ) } } From ac01da5f4a29191971f82123bbeac41ac6386bf6 Mon Sep 17 00:00:00 2001 From: krushimir <189111540+krushimir@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:30:45 +0100 Subject: [PATCH 2/3] feat(batch-kernel): verify TRANSACTIONS_COMMITMENT and check duplicate TX_IDs --- Cargo.lock | 1 + .../asm/kernels/batch/lib/epilogue.masm | 19 +- .../asm/kernels/batch/lib/prologue.masm | 312 ++++++++++-------- crates/miden-protocol/src/batch/kernel/mod.rs | 10 +- .../src/kernel_tests/batch/test_prologue.rs | 46 +++ crates/miden-tx-batch-prover/Cargo.toml | 6 +- 6 files changed, 244 insertions(+), 150 deletions(-) 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/epilogue.masm b/crates/miden-protocol/asm/kernels/batch/lib/epilogue.masm index 4b89188d6f..d31d32ab31 100644 --- a/crates/miden-protocol/asm/kernels/batch/lib/epilogue.masm +++ b/crates/miden-protocol/asm/kernels/batch/lib/epilogue.masm @@ -10,14 +10,25 @@ use miden::core::sys #! Computes output commitments and builds the output stack. #! -#! Output stack layout: -#! [INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_SMT_ROOT, batch_expiration_block_num, ...] +#! Output stack layout (positions 0-15, top to bottom): +#! [INPUT_NOTES_COMMITMENT(4), OUTPUT_NOTES_SMT_ROOT(4), batch_expiration(1), zeros(7)] #! -#! Inputs: [] -#! Outputs: [INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_SMT_ROOT, batch_expiration_block_num, ...] +#! 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/prologue.masm b/crates/miden-protocol/asm/kernels/batch/lib/prologue.masm index 588f9c8c93..1c4f7b0393 100644 --- a/crates/miden-protocol/asm/kernels/batch/lib/prologue.masm +++ b/crates/miden-protocol/asm/kernels/batch/lib/prologue.masm @@ -1,194 +1,226 @@ #! Batch Kernel Prologue #! -#! This module handles the initialization phase of the batch kernel: -#! - Unhashing the block hash to extract block header fields -#! - Unhashing the transactions commitment to get the list of (TX_ID, account_id) pairs -#! - Validating that the batch is not empty -#! - Setting up memory with initial data +#! 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 per batch +# Maximum number of transactions allowed in a single batch const MAX_TRANSACTIONS_PER_BATCH=4 -# Error constants +# 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. #! -#! This procedure: -#! 1. Takes BLOCK_HASH and TRANSACTIONS_COMMITMENT from the stack -#! 2. Unhashes BLOCK_HASH to get block header fields (from advice) -#! 3. Unhashes TRANSACTIONS_COMMITMENT to get [(TX_ID, account_id), ...] (from advice) -#! 4. Validates batch is not empty -#! 5. Verifies transaction list hashes to TRANSACTIONS_COMMITMENT -#! 6. Stores all data in memory for subsequent phases -#! -#! Inputs: [BLOCK_HASH, TRANSACTIONS_COMMITMENT] -#! Outputs: [] -#! -#! Errors: -#! - ERR_BATCH_EMPTY: batch contains no transactions -#! - ERR_BATCH_TOO_LARGE: batch exceeds MAX_TRANSACTIONS_PER_BATCH -#! - ERR_TX_COMMITMENT_MISMATCH: transaction list does not hash to TRANSACTIONS_COMMITMENT +#! Inputs: +#! Operand stack: [BLOCK_HASH, TRANSACTIONS_COMMITMENT] +#! Outputs: +#! Operand stack: [] pub proc prepare_batch - # ========================================================================= - # PHASE 1: Process BLOCK_HASH - # ========================================================================= - # Initial stack: [BLOCK_HASH, TRANSACTIONS_COMMITMENT] - - # Store BLOCK_HASH in memory (needed for MMR authentication later) + # Store BLOCK_HASH and load block header fields from advice exec.memory::set_block_hash - # Stack: [TRANSACTIONS_COMMITMENT, ...] - - # Read block_num from advice and store - adv_push.1 - exec.memory::set_block_num - # Stack: [TRANSACTIONS_COMMITMENT, ...] - - # Read CHAIN_COMMITMENT from advice and store - adv_push.4 - exec.memory::set_chain_commitment - # Stack: [TRANSACTIONS_COMMITMENT, ...] - - + # => [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 - # Stack: [] - + # => [] + # Load and validate transaction count adv_push.1 - # Stack: [tx_count] - - # Validate: batch not empty + # => [tx_count] + dup eq.0 assertz.err=ERR_BATCH_EMPTY - - # Validate: batch not too large dup push.MAX_TRANSACTIONS_PER_BATCH lte assert.err=ERR_BATCH_TOO_LARGE - - # Store transaction count exec.memory::set_transaction_count - # Stack: [] - - # Load transaction list from advice + # => [] + + # Load and verify transaction list exec.load_and_verify_transaction_list - - # Load transaction preimages from advice + exec.check_duplicate_tx_ids exec.load_transaction_preimages - - # Initialize batch expiration to max u32 (minimum across txs computed later) - push.0xFFFFFFFF - exec.memory::set_batch_expiration_block_num + # => [] + + # Initialize batch expiration to max u32 (computed from tx expirations later) + push.0xFFFFFFFF exec.memory::set_batch_expiration_block_num + # => [] end -#! Loads the transaction list from advice, verifies it hashes to TRANSACTIONS_COMMITMENT, -#! and stores the data in memory. -#! -#! The hash format for BatchId is: [(TX_ID, account_id_prefix, account_id_suffix, 0, 0), ...] -#! Each transaction contributes 8 felts (2 words). +# TRANSACTION DATA +# ================================================================================================= + +#! Loads transaction list from advice, stores in memory, and verifies hash. #! -#! Inputs: [] -#! Outputs: [] +#! Hash format: [(TX_ID, account_id_prefix, account_id_suffix, 0, 0), ...] per transaction. #! -#! Errors: -#! - ERR_TX_COMMITMENT_MISMATCH if hash doesn't match +#! Inputs: +#! Operand stack: [] +#! Outputs: +#! Operand stack: [] proc load_and_verify_transaction_list - exec.memory::get_transaction_count - push.0 + 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 - # => [tx_count > i, i, tx_count] - - if.true - # Compute pointers for this transaction - 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 TX_ID and store in both structured storage and hash buffer - padw adv_loadw - dup.4 mem_storew_be - dup.5 mem_storew_be dropw - # => [tx_ptr, hash_ptr, i, tx_count] - - # Load and store account_id_prefix - adv_push.1 - dup dup.2 add.4 mem_store - dup.2 add.4 mem_store - # => [tx_ptr, hash_ptr, i, tx_count] - - # Load and store account_id_suffix - 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] - - # Increment loop counter - drop drop add.1 - push.1 - else - push.0 - end + # => [should_loop, i+1, tx_count] end - + drop # => [tx_count] - - # TODO: Verify hash of transaction data matches TRANSACTIONS_COMMITMENT - drop + + # 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 tx: INIT_STATE, FINAL_STATE, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, expiration #! -#! Inputs: [] -#! Outputs: [] +#! 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 + 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 - # => [tx_count > i, i, tx_count] - - if.true - dup exec.memory::get_tx_data_ptr - # => [tx_ptr, i, tx_count] - - # Load account state commitments - 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 - - # Load expiration_block_num - adv_push.1 dup.1 add.6 mem_store - # => [tx_ptr, i, tx_count] - - drop add.1 - push.1 - else - push.0 + # => [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/src/batch/kernel/mod.rs b/crates/miden-protocol/src/batch/kernel/mod.rs index e32043aebb..9d22b9adb8 100644 --- a/crates/miden-protocol/src/batch/kernel/mod.rs +++ b/crates/miden-protocol/src/batch/kernel/mod.rs @@ -90,12 +90,12 @@ impl BatchKernel { /// [BLOCK_HASH, TRANSACTIONS_COMMITMENT] /// ``` pub fn build_input_stack(block_hash: Word, batch_id: BatchId) -> StackInputs { - let mut inputs: Vec = Vec::with_capacity(8); - // BatchId (TRANSACTIONS_COMMITMENT) - will be below block_hash - inputs.extend(batch_id.as_elements()); - // Block hash - will be at top of stack + let mut inputs: Vec = Vec::with_capacity(16); inputs.extend(block_hash); - // VM auto-pads to 16 elements with zeros + // 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()) diff --git a/crates/miden-testing/src/kernel_tests/batch/test_prologue.rs b/crates/miden-testing/src/kernel_tests/batch/test_prologue.rs index 6c6528c6bc..b2243cb80b 100644 --- a/crates/miden-testing/src/kernel_tests/batch/test_prologue.rs +++ b/crates/miden-testing/src/kernel_tests/batch/test_prologue.rs @@ -1,5 +1,6 @@ //! Tests for the batch kernel prologue. +use alloc::string::ToString; use alloc::sync::Arc; use alloc::vec::Vec; @@ -92,3 +93,48 @@ async fn test_batch_prologue_basic() -> anyhow::Result<()> { 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(()) +} diff --git a/crates/miden-tx-batch-prover/Cargo.toml b/crates/miden-tx-batch-prover/Cargo.toml index d664da1d37..6cf4592d7b 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-protocol/testing", "miden-air/testing"] [dependencies] miden-protocol = { workspace = true } miden-tx = { workspace = true } + +# Only needed for ExecutionProof::new_dummy() in testing mode +miden-air = { workspace = true, optional = true } From facfa1ae5157b7e3779cbadaca688e35aba6b76a Mon Sep 17 00:00:00 2001 From: krushimir <189111540+krushimir@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:37:45 +0100 Subject: [PATCH 3/3] feat(batch-kernel): implement transaction expiration validation - add expiration check: `tx_expiration` > `block_num` - compute batch_expiration as min of all tx expirations - add placeholder for future STARK proof verification - add test for expired transaction rejection - update test to verify correct `batch_expiration` value - fix `clippy` issues --- .../asm/kernels/batch/lib/transaction.masm | 69 ++++++++++---- .../asm/kernels/batch/main.masm | 8 +- .../src/batch/kernel/advice_inputs.rs | 19 ++-- crates/miden-protocol/src/batch/kernel/mod.rs | 35 +++---- .../miden-protocol/src/batch/proven_batch.rs | 3 +- .../src/kernel_tests/batch/test_prologue.rs | 94 ++++++++++++------- crates/miden-tx-batch-prover/Cargo.toml | 4 +- 7 files changed, 146 insertions(+), 86 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/batch/lib/transaction.masm b/crates/miden-protocol/asm/kernels/batch/lib/transaction.masm index 22f866a832..c84768d088 100644 --- a/crates/miden-protocol/asm/kernels/batch/lib/transaction.masm +++ b/crates/miden-protocol/asm/kernels/batch/lib/transaction.masm @@ -1,7 +1,6 @@ #! Batch Kernel Transaction Module #! #! This module handles transaction processing: -#! - Loading transaction data from advice provider #! - Validating transaction expiration #! - Tracking minimum expiration for batch #! @@ -10,11 +9,10 @@ use $kernel::memory -# CONSTANTS +# ERRORS # ================================================================================================= -# Error: transaction has expired -const ERR_BATCH_TX_EXPIRED="transaction has expired relative to batch reference block" +const ERR_TX_EXPIRED="transaction has expired relative to batch reference block" # TRANSACTION PROCESSING # ================================================================================================= @@ -22,23 +20,56 @@ const ERR_BATCH_TX_EXPIRED="transaction has expired relative to batch reference #! Processes all transactions in the batch. #! #! For each transaction: -#! 1. Load transaction data from advice (tx_id is the commitment) -#! 2. Validate transaction hasn't expired -#! 3. Track minimum expiration for batch expiration +#! 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: [] -#! Outputs: [] -#! -#! Errors: -#! - ERR_BATCH_TX_EXPIRED: transaction expired before batch reference block +#! Inputs: +#! Operand stack: [] +#! Outputs: +#! Operand stack: [] pub proc process_transactions - # TODO: Implement transaction processing loop - # 1. Get transaction count from memory - # 2. Loop over each transaction - # 3. For each: load data, validate expiration, track min expiration - - # For now, this is a placeholder that does nothing - push.0 drop + 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 index c205fa3e0d..70d6bbbcfb 100644 --- a/crates/miden-protocol/asm/kernels/batch/main.masm +++ b/crates/miden-protocol/asm/kernels/batch/main.masm @@ -74,7 +74,13 @@ proc main # Check note constraints (no duplicates, counts within limits) exec.note::validate_notes - # PHASE 4: EPILOGUE + # 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 diff --git a/crates/miden-protocol/src/batch/kernel/advice_inputs.rs b/crates/miden-protocol/src/batch/kernel/advice_inputs.rs index 1544d87cc9..8f7f3e7af7 100644 --- a/crates/miden-protocol/src/batch/kernel/advice_inputs.rs +++ b/crates/miden-protocol/src/batch/kernel/advice_inputs.rs @@ -33,24 +33,20 @@ impl BatchAdviceInputs { /// /// 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 { + 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()); @@ -72,18 +68,17 @@ impl BatchAdviceInputs { 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 + // 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), + inner: AdviceInputs::default().with_stack(advice_stack).with_map(advice_map), } } diff --git a/crates/miden-protocol/src/batch/kernel/mod.rs b/crates/miden-protocol/src/batch/kernel/mod.rs index 9d22b9adb8..e1b7782b8d 100644 --- a/crates/miden-protocol/src/batch/kernel/mod.rs +++ b/crates/miden-protocol/src/batch/kernel/mod.rs @@ -23,8 +23,7 @@ pub use advice_inputs::BatchAdviceInputs; 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") + Program::read_from_bytes(kernel_main_bytes).expect("failed to deserialize batch kernel runtime") }); // BATCH KERNEL ERROR @@ -119,28 +118,30 @@ impl BatchKernel { // 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( + 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( + 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( + 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(), - ))? + .map_err(|_| { + BatchKernelError::InvalidOutputStack( + "batch expiration block number should be smaller than u32::MAX".to_string(), + ) + })? .into(); Ok(BatchKernelOutputs { diff --git a/crates/miden-protocol/src/batch/proven_batch.rs b/crates/miden-protocol/src/batch/proven_batch.rs index 98d8e8d628..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; @@ -10,7 +11,6 @@ use crate::note::Nullifier; use crate::transaction::{InputNoteCommitment, InputNotes, OrderedTransactionHeaders, OutputNote}; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; use crate::vm::ExecutionProof; -use crate::Word; /// A transaction batch with an execution proof. /// @@ -42,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, diff --git a/crates/miden-testing/src/kernel_tests/batch/test_prologue.rs b/crates/miden-testing/src/kernel_tests/batch/test_prologue.rs index b2243cb80b..bfe2baf896 100644 --- a/crates/miden-testing/src/kernel_tests/batch/test_prologue.rs +++ b/crates/miden-testing/src/kernel_tests/batch/test_prologue.rs @@ -6,14 +6,14 @@ use alloc::vec::Vec; use miden_processor::DefaultHost; use miden_processor::fast::FastProcessor; -use rand::Rng; use miden_protocol::account::{Account, AccountStorageMode}; -use miden_protocol::batch::kernel::{BatchAdviceInputs, BatchKernel}; 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}; @@ -40,23 +40,17 @@ async fn test_batch_prologue_basic() -> anyhow::Result<()> { 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 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)]; @@ -68,28 +62,27 @@ async fn test_batch_prologue_basic() -> anyhow::Result<()> { // 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 the placeholder max value + + // Verify batch_expiration is min(tx1.expiration=1000, tx2.expiration=500) = 500 assert_eq!( parsed.batch_expiration_block_num, - BlockNumber::from(0xFFFFFFFF_u32), - "batch_expiration should be max u32 (placeholder)" + 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 - // - batch_expiration equals min(tx1.expiration, tx2.expiration) = 500 Ok(()) } @@ -104,14 +97,10 @@ async fn test_batch_prologue_rejects_duplicate_tx_ids() -> anyhow::Result<()> { 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()?; + 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)]; @@ -138,3 +127,40 @@ async fn test_batch_prologue_rejects_duplicate_tx_ids() -> anyhow::Result<()> { 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 6cf4592d7b..71d1f49c49 100644 --- a/crates/miden-tx-batch-prover/Cargo.toml +++ b/crates/miden-tx-batch-prover/Cargo.toml @@ -19,11 +19,11 @@ bench = false default = ["std"] std = ["miden-protocol/std", "miden-tx/std"] # miden-air/testing provides ExecutionProof::new_dummy() -testing = ["miden-protocol/testing", "miden-air/testing"] +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 = { workspace = true, optional = true } +miden-air = { optional = true, workspace = true }