Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
- [BREAKING] Renamed account ID version 0 to version 1 and made encoded version 0 invalid ([#2842](https://github.com/0xMiden/protocol/issues/2842)).
- [BREAKING] Changed note metadata version 1 to encode as `1`, leaving encoded version `0` invalid.
- Documented the `miden::protocol::account_id` module in the protocol library docs ([#2607](https://github.com/0xMiden/protocol/issues/2607)).
- Added a skeleton batch kernel program with the public input/output contract from issue [#1122](https://github.com/0xMiden/protocol/issues/1122), wired through `LocalBatchProver::prove` and attached to `ProvenBatch` as an `ExecutionProof`. The kernel does not yet perform any verification; the verification chain that fills in the real outputs will land in a follow-up PR.
Comment thread
mmagician marked this conversation as resolved.
Outdated

### Fixes

Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions crates/miden-protocol/asm/kernels/batch/main.masm
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# MAIN
# =================================================================================================

#! Batch kernel program (skeleton).
#!
#! A transaction batch groups a set of independently-proven transactions so they can later be
#! aggregated into a block by the block kernel. This program defines the public input/output
#! contract that the batch kernel will eventually verify, but does not yet perform any
#! verification: it drops its inputs and exits, leaving the all-zero word output region as the
Comment thread
mmagician marked this conversation as resolved.
Outdated
#! stack's initial padding zeros.
#!
#! Splitting the kernel into "shape only" and "verification" lets downstream Rust tooling
#! (`BatchKernel`'s input/advice/output builders, `LocalBatchProver::prove`, the `ProvenBatch`
#! proof field) be built and reviewed against a stable interface before the verification logic
#! lands. The verification chain that fills in the real outputs is added in a follow-up PR.
#!
Comment thread
mmagician marked this conversation as resolved.
Outdated
#! Inputs: [
#! TRANSACTIONS_COMMITMENT,
#! BLOCK_HASH,
#! pad(8),
#! ]
#!
#! Outputs: [
#! INPUT_NOTES_COMMITMENT,
#! OUTPUT_NOTES_COMMITMENT,
#! batch_expiration_block_num,
#! pad(7),
#! ]
#!
#! Where:
#! - TRANSACTIONS_COMMITMENT is the sequential hash of the `(tx_id, account_id)` tuples
#! committing to the transactions in the batch (i.e. the `BatchId` value).
#! - BLOCK_HASH is the commitment of the batch's reference block.
#! - INPUT_NOTES_COMMITMENT will be the sequential hash over every transaction's input note
#! commitments. In this skeleton it is the empty word.
#! - OUTPUT_NOTES_COMMITMENT will be the sequential hash over every transaction's output note
#! commitments. In this skeleton it is the empty word.
#! - batch_expiration_block_num will be the minimum of every transaction's
#! `expiration_block_num`. In this skeleton it is zero.
#!
#! TODO: replace this skeleton with the verification chain that reconstructs every transaction
#! from the advice provider, anchors the reconstruction in `TRANSACTIONS_COMMITMENT`, and
#! emits the real batch commitments and expiration.
Comment thread
mmagician marked this conversation as resolved.
Outdated
proc main
# Drop TRANSACTIONS_COMMITMENT and BLOCK_HASH. The VM keeps the stack at depth >= 16, so the
# remaining 8 felts (the explicit `pad(8)` inputs) plus 8 zeros auto-filled below them form
# the all-zero 16-felt output region.
Comment thread
mmagician marked this conversation as resolved.
Outdated
dropw dropw
end

begin
exec.main
end
27 changes: 26 additions & 1 deletion crates/miden-protocol/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const ASM_PROTOCOL_DIR: &str = "protocol";
const SHARED_UTILS_DIR: &str = "shared_utils";
const SHARED_MODULES_DIR: &str = "shared_modules";
const ASM_TX_KERNEL_DIR: &str = "kernels/transaction";
const ASM_BATCH_KERNEL_DIR: &str = "kernels/batch";

const PROTOCOL_LIB_NAMESPACE: &str = "miden::protocol";

Expand All @@ -30,7 +31,7 @@ const PROTOCOL_LIB_ERRORS_RS_FILE: &str = "protocol_errors.rs";
const TX_KERNEL_ERRORS_ARRAY_NAME: &str = "TX_KERNEL_ERRORS";
const PROTOCOL_LIB_ERRORS_ARRAY_NAME: &str = "PROTOCOL_LIB_ERRORS";

const TX_KERNEL_ERROR_CATEGORIES: [&str; 14] = [
const TX_KERNEL_ERROR_CATEGORIES: [&str; 15] = [
"KERNEL",
"PROLOGUE",
"EPILOGUE",
Expand All @@ -45,6 +46,7 @@ const TX_KERNEL_ERROR_CATEGORIES: [&str; 14] = [
"LINK_MAP",
"INPUT_NOTE",
"OUTPUT_NOTE",
"BATCH",
];

// PRE-PROCESSING
Expand Down Expand Up @@ -85,13 +87,36 @@ fn main() -> Result<()> {
let protocol_lib = compile_protocol_lib(&source_dir, &target_dir, assembler.clone())?;
assembler.link_dynamic_library(protocol_lib)?;

// compile batch kernel
compile_batch_kernel(&source_dir, &target_dir.join("kernels"))?;

generate_error_constants(&source_dir, &build_dir)?;

generate_event_constants(&source_dir, &target_dir)?;

Ok(())
}

// COMPILE BATCH KERNEL
// ================================================================================================

/// Reads the batch kernel MASM source from the `source_dir`, compiles it, and saves the result
/// to the `target_dir` as a `batch_kernel.masb` binary file.
///
/// Unlike the transaction kernel, the batch kernel does not expose syscalls, so there is no
/// `KernelLibrary` to build — only a single executable program assembled from
/// `kernels/batch/main.masm`.
Comment thread
mmagician marked this conversation as resolved.
Outdated
fn compile_batch_kernel(source_dir: &Path, target_dir: &Path) -> Result<()> {
let batch_kernel_dir = source_dir.join(ASM_BATCH_KERNEL_DIR);
let main_file_path = batch_kernel_dir.join("main.masm");

let assembler = build_assembler(None)?;
let batch_main = assembler.assemble_program(main_file_path)?;

let masb_file_path = target_dir.join("batch_kernel.masb");
batch_main.write_to_file(masb_file_path).into_diagnostic()
}

// COMPILE TRANSACTION KERNEL
// ================================================================================================

Expand Down
177 changes: 177 additions & 0 deletions crates/miden-protocol/src/batch/kernel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use alloc::vec::Vec;

use miden_core::program::Kernel;

use crate::batch::ProposedBatch;
use crate::block::BlockNumber;
use crate::errors::BatchOutputError;
use crate::utils::serde::Deserializable;
use crate::utils::sync::LazyLock;
use crate::vm::{AdviceInputs, Program, ProgramInfo, StackInputs, StackOutputs};
use crate::{Felt, Word};

// CONSTANTS
// ================================================================================================

static KERNEL_MAIN: LazyLock<Program> = LazyLock::new(|| {
let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/kernels/batch_kernel.masb"));
Program::read_from_bytes(bytes).expect("failed to deserialize batch kernel runtime")
});

// Output stack indices, kept in sync with the layout at the end of `main.masm::main`. These are
// felt offsets, not word indices: `get_word(N)` returns the four felts at positions `N..N+4`.
Comment thread
mmagician marked this conversation as resolved.
Outdated
const INPUT_NOTES_COMMITMENT_WORD_IDX: usize = 0;
const OUTPUT_NOTES_COMMITMENT_WORD_IDX: usize = 4;
const BATCH_EXPIRATION_BLOCK_NUM_ELEMENT_IDX: usize = 8;
// The word containing `batch_expiration_block_num` plus three padding zeros.
const EXPIRATION_PAD_WORD_FELT_IDX: usize = 8;
const EXPIRATION_PAD_WORD_INNER_OFFSET: usize = 1;
// The trailing word at felt indices 12..16 must be all zero.
const TRAILING_PAD_WORD_FELT_IDX: usize = 12;

// BATCH KERNEL
// ================================================================================================

/// The batch kernel program: an executable Miden program that proves a batch of transactions.
///
/// The kernel takes `[TRANSACTIONS_COMMITMENT, BLOCK_HASH]` as public inputs and emits
/// `[INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, batch_expiration_block_num]`. See
/// `asm/kernels/batch/main.masm` for the input/output contract and the `TODO` listing checks
/// the kernel does not yet enforce.
pub struct BatchKernel;

impl BatchKernel {
// KERNEL SOURCE CODE
// --------------------------------------------------------------------------------------------

/// Returns the executable batch kernel program loaded from the build's `OUT_DIR`.
pub fn main() -> Program {
KERNEL_MAIN.clone()
}

/// Returns [`ProgramInfo`] for the batch kernel program.
///
/// The batch kernel does not expose syscalls, so the associated [`Kernel`] is empty.
pub fn program_info() -> ProgramInfo {
ProgramInfo::new(Self::main().hash(), Kernel::default())
}

// INPUT BUILDERS
// --------------------------------------------------------------------------------------------

/// Transforms the provided [`ProposedBatch`] into the stack and advice inputs needed to
/// execute the batch kernel.
pub fn prepare_inputs(proposed_batch: &ProposedBatch) -> (StackInputs, AdviceInputs) {
let block_hash = proposed_batch.reference_block_header().commitment();
let transactions_commitment = proposed_batch.id().as_word();

let stack_inputs = Self::build_input_stack(block_hash, transactions_commitment);
let advice_inputs = Self::build_advice_inputs(proposed_batch);

(stack_inputs, advice_inputs)
}

/// Returns the stack with the public inputs required by the batch kernel.
///
/// The initial stack is:
///
/// ```text
/// [TRANSACTIONS_COMMITMENT, BLOCK_HASH, pad(8)]
/// ```
///
/// Where:
/// - `TRANSACTIONS_COMMITMENT` is the value [`BatchId`](crate::batch::BatchId) computes — a
/// sequential hash of `(transaction_id || account_id_prefix || account_id_suffix || 0 || 0)`
/// over all transactions in the batch.
/// - `BLOCK_HASH` is the commitment of the batch's reference block.
pub fn build_input_stack(block_hash: Word, transactions_commitment: Word) -> StackInputs {
let mut inputs: Vec<Felt> = Vec::with_capacity(8);
inputs.extend_from_slice(transactions_commitment.as_elements());
inputs.extend_from_slice(block_hash.as_elements());

StackInputs::new(&inputs).expect("number of stack inputs should be <= 16")
}

/// Builds the stack with the expected batch kernel outputs.
///
/// The output stack is defined as:
///
/// ```text
/// [INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, batch_expiration_block_num]
/// ```
pub fn build_output_stack(
input_notes_commitment: Word,
output_notes_commitment: Word,
batch_expiration_block_num: BlockNumber,
) -> StackOutputs {
let mut outputs: Vec<Felt> = Vec::with_capacity(9);
outputs.extend_from_slice(input_notes_commitment.as_elements());
outputs.extend_from_slice(output_notes_commitment.as_elements());
outputs.push(Felt::from(batch_expiration_block_num));

StackOutputs::new(&outputs).expect("number of stack outputs should be <= 16")
}

/// Extracts batch output data from the provided stack outputs.
///
/// # Errors
///
/// Returns an error if:
/// - The padding cells (positions 9..16) are not all zero.
/// - `batch_expiration_block_num` does not fit into a `u32`.
pub fn parse_output_stack(
stack: &StackOutputs,
) -> Result<(Word, Word, BlockNumber), BatchOutputError> {
let input_notes_commitment = stack
.get_word(INPUT_NOTES_COMMITMENT_WORD_IDX)
.expect("input_notes_commitment word missing");
let output_notes_commitment = stack
.get_word(OUTPUT_NOTES_COMMITMENT_WORD_IDX)
.expect("output_notes_commitment word missing");

let expiration_felt = stack
.get_element(BATCH_EXPIRATION_BLOCK_NUM_ELEMENT_IDX)
.expect("batch_expiration_block_num missing");

// The word at felt indices 8..12 contains [batch_expiration_block_num, 0, 0, 0]. Indices
// 9..12 of the output stack must be zero.
let pad_word = stack
.get_word(EXPIRATION_PAD_WORD_FELT_IDX)
.expect("expiration pad word missing");
if pad_word.as_elements()[EXPIRATION_PAD_WORD_INNER_OFFSET..]
!= Word::empty().as_elements()[1..]
{
return Err(BatchOutputError::OutputStackInvalid(
"batch_expiration_block_num must be followed by zero padding".into(),
));
}

// Felts 12..16 (the trailing word) must also be zero.
let trailing_word =
stack.get_word(TRAILING_PAD_WORD_FELT_IDX).expect("trailing word missing");
if trailing_word != Word::empty() {
return Err(BatchOutputError::OutputStackInvalid(
"trailing output stack cells must be zero".into(),
));
}
Comment thread
mmagician marked this conversation as resolved.
Outdated

let batch_expiration_block_num = u32::try_from(expiration_felt.as_canonical_u64())
.map_err(|_| BatchOutputError::ExpirationBlockNumberTooLarge(expiration_felt))?
.into();

Ok((input_notes_commitment, output_notes_commitment, batch_expiration_block_num))
}

// ADVICE BUILDER
// --------------------------------------------------------------------------------------------

/// Builds the advice inputs (map + stack) consumed by the batch kernel.
///
/// The skeleton kernel ignores its advice inputs, so this returns the default empty value.
/// The follow-up PR that adds the verification chain will populate the advice map with the
/// `(tx_id, account_id)` tuple list keyed by `TRANSACTIONS_COMMITMENT` and the per-tx headers
/// and note tuples.
Comment thread
mmagician marked this conversation as resolved.
Outdated
fn build_advice_inputs(_proposed_batch: &ProposedBatch) -> AdviceInputs {
AdviceInputs::default()
}
}
3 changes: 3 additions & 0 deletions crates/miden-protocol/src/batch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ pub use ordered_batches::OrderedBatches;

mod input_output_note_tracker;
pub(crate) use input_output_note_tracker::InputOutputNoteTracker;

mod kernel;
pub use kernel::BatchKernel;
5 changes: 5 additions & 0 deletions crates/miden-protocol/src/batch/proposed_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ impl ProposedBatch {
self.id
}

/// Returns the header of the reference block this batch is proposed for.
pub fn reference_block_header(&self) -> &BlockHeader {
&self.reference_block_header
}

/// Returns the block number at which the batch will expire.
pub fn batch_expiration_block_num(&self) -> BlockNumber {
self.batch_expiration_block_num
Expand Down
Loading
Loading