Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1cbd5f9
feat: add skeleton batch kernel + ProvenBatch proof field
claude May 12, 2026
0411259
Apply suggestions from code review
mmagician May 27, 2026
ee2df53
chore(protocol): drop premature BATCH error category
claude May 27, 2026
c508740
refactor(protocol): order build_input_stack params to match stack layout
claude May 27, 2026
9ed96f7
refactor(batch-prover): remove unused proof_security_level accessor
claude May 27, 2026
52adb85
refactor(protocol): simplify batch output padding check
claude May 27, 2026
ee3f559
test(batch): reuse shared chain setup helpers
claude May 27, 2026
e3d15c5
docs: trim verbose batch kernel CHANGELOG entry
claude May 27, 2026
ba9af23
Merge remote-tracking branch 'origin/next' into mmagician-claude/batc…
claude May 27, 2026
0d20126
Merge remote-tracking branch 'origin/next' into mmagician-claude/batc…
claude May 29, 2026
11f0760
refactor(protocol): name batch kernel inputs BATCH_ID and BLOCK_COMMI…
claude May 29, 2026
7a4afcb
refactor(protocol): use Felt::ZERO associated constant
claude May 29, 2026
26c044b
refactor(protocol): type batch kernel errors instead of stringifying
claude May 29, 2026
7c72675
test(batch): rename batch_kernel module to test_batch_kernel
claude May 29, 2026
b84ea28
chore: shorten comment
mmagician May 29, 2026
5e25d94
chore: fmt
mmagician May 29, 2026
d812d4d
refactor(batch-prover): split batch execution and proving
claude May 29, 2026
7f3a5df
Merge branch 'next' into mmagician-claude/batch-kernel-skeleton
mmagician May 29, 2026
fc7d855
Update crates/miden-tx-batch-prover/src/batch_executor.rs
mmagician May 29, 2026
79165e9
refactor(protocol): return a BatchOutput from parse_output_stack
claude May 29, 2026
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 @@ -3,6 +3,7 @@
## v0.16.0 (TBD)

### Changes
- Added a skeleton batch kernel ([#1122](https://github.com/0xMiden/protocol/issues/1122)) wired through `LocalBatchProver::prove` and attached to `ProvenBatch` as an `ExecutionProof`. It does not yet perform any verification.

- [BREAKING] Renamed `AccountStorageDelta` to `AccountStoragePatch` ([#3002](https://github.com/0xMiden/protocol/pull/3002)).
- [BREAKING] Extracted `NullifierTreeBackendReader` and `AccountTreeBackendReader` traits from existing `NullifierTreeBackend` and `AccountTreeBackend` traits ([#2755](https://github.com/0xMiden/protocol/pull/2755)).
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.

41 changes: 41 additions & 0 deletions crates/miden-protocol/asm/kernels/batch/main.masm
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 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 currently does not yet perform
#! any verification: it drops its inputs and exits, leaving the all-zero word output region as the
#! stack's initial padding zeros.
#!
#! Inputs: [
#! BATCH_ID,
#! BLOCK_COMMITMENT,
#! pad(8),
#! ]
#!
#! Outputs: [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the original intention here was for the batch kernel to output the root of the BatchNoteTree, rather than just a commitment to the output notes.

This way, the block kernel would have to do much less work and could aggregate the individual BatchNoteTrees into a single BlockNoteTree. This would have to be the MASM equivalent of BlockNoteTree::insert_batch_note_subtree.

I think the reason we don't currently do this at block level, is because we decided to also have note erasure at block level, and so if the block erases on of the batch's notes, the tree root would become stale and would have to be updated. Doing this update is probably still better than recomputing the entire tree from scratch, but we may want to think about that.

To start simple, I see two routes:

  • Let the batch output the BatchNoteTree root and aggregate at block level, don't do note erasure at block level for now.
  • Let the batch output a list of output notes and let the block kernel do the full aggregation. We can optimize this whole process later.

I think the first route is a bit closer to the end goal, so I'd go with that one. Just a suggestion, though.

I think I'm skeptical of the usefulness of note erasure at block level, given that batches are already incentivized to do as much note erasure as possible, and it just adds extra complexity.

Copy link
Copy Markdown
Collaborator Author

@mmagician mmagician May 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm skeptical of the usefulness of note erasure at block level, given that batches are already incentivized to do as much note erasure as possible, and it just adds extra complexity.

Agreed, my guess is that this will rarely if ever be used in practice. I'd be in favor of dropping it. Created #3008

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the original intention here was for the batch kernel to output the root of the BatchNoteTree, rather than just a commitment to the output notes.

Yes I know, I should have maybe mentioned this in the comments that the first version would just have a commitment to all output notes instead. Mostly because, AFAICS, the BatchNoteTree is not actually wired up anywhere during batch construction.

So what I'd suggest is:

  1. continue with the skeleton batch kernel to reference OUTPUT_NOTES_COMMITMENT
  2. wire up the construction of BatchNoteTree as part of ProposedBatch::new()
  3. change the batch kernel to output (and verify) BATCH_NOTE_TREE_COMMITMENT

WDYT? If so I'll create an issue for 2.

#! INPUT_NOTES_COMMITMENT,
#! OUTPUT_NOTES_COMMITMENT,
#! batch_expiration_block_num,
#! pad(7),
#! ]
#!
#! Where:
#! - BATCH_ID is the batch's `BatchId`, the commitment to its transactions.
#! - BLOCK_COMMITMENT 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.
#!
proc main
dropw dropw
end

begin
exec.main
end
20 changes: 20 additions & 0 deletions 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 Down Expand Up @@ -85,13 +86,32 @@ 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.
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
150 changes: 150 additions & 0 deletions crates/miden-protocol/src/batch/kernel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use alloc::vec::Vec;

use miden_core::program::Kernel;

use crate::batch::{BatchOutput, 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")
});

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

/// The batch kernel program: an executable Miden program that proves a batch of transactions.
///
/// The kernel takes `[BATCH_ID, BLOCK_COMMITMENT]` 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.
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_commitment = proposed_batch.reference_block_header().commitment();
let batch_id = proposed_batch.id().as_word();

let stack_inputs = Self::build_input_stack(batch_id, block_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
/// [BATCH_ID, BLOCK_COMMITMENT, pad(8)]
/// ```
///
/// Where:
/// - `BATCH_ID` is the batch's [`BatchId`](crate::batch::BatchId).
/// - `BLOCK_COMMITMENT` is the commitment of the batch's reference block.
pub fn build_input_stack(batch_id: Word, block_commitment: Word) -> StackInputs {
let mut inputs: Vec<Felt> = Vec::with_capacity(8);
inputs.extend_from_slice(batch_id.as_elements());
inputs.extend_from_slice(block_commitment.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 the [`BatchOutput`] 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<BatchOutput, BatchOutputError> {
let input_notes_commitment = stack
Comment thread
mmagician marked this conversation as resolved.
.get_word(BatchOutput::INPUT_NOTES_COMMITMENT_WORD_IDX)
.expect("input_notes_commitment word missing");
let output_notes_commitment = stack
.get_word(BatchOutput::OUTPUT_NOTES_COMMITMENT_WORD_IDX)
.expect("output_notes_commitment word missing");

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

// Every cell after batch_expiration_block_num must be zero padding.
if stack[BatchOutput::BATCH_EXPIRATION_BLOCK_NUM_ELEMENT_IDX + 1..]
.iter()
.any(|&felt| felt != Felt::ZERO)
{
return Err(BatchOutputError::OutputStackInvalid(
"batch_expiration_block_num must be followed by zero padding".into(),
));
}

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

Ok(BatchOutput::new(
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.
fn build_advice_inputs(_proposed_batch: &ProposedBatch) -> AdviceInputs {
AdviceInputs::default()
}
}
6 changes: 6 additions & 0 deletions crates/miden-protocol/src/batch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ mod ordered_batches;
pub use ordered_batches::OrderedBatches;

pub(super) mod note_tracker;

mod kernel;
pub use kernel::BatchKernel;

mod output;
pub use output::BatchOutput;
66 changes: 66 additions & 0 deletions crates/miden-protocol/src/batch/output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use crate::Word;
use crate::block::BlockNumber;

// BATCH OUTPUT
// ================================================================================================

/// The public outputs produced by the batch kernel.
///
/// This is the parsed, typed form of the kernel's output stack (see
/// [`BatchKernel::parse_output_stack`](crate::batch::BatchKernel::parse_output_stack)), mirroring
/// [`TransactionOutputs`](crate::transaction::TransactionOutputs) for transactions.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BatchOutput {
/// The commitment to the batch's input notes.
input_notes_commitment: Word,
/// The commitment to the batch's output notes.
output_notes_commitment: Word,
/// The block number at which the batch expires.
batch_expiration_block_num: BlockNumber,
}

impl BatchOutput {
// OUTPUT STACK LAYOUT
// --------------------------------------------------------------------------------------------

/// The element index at which the input notes commitment word starts on the output stack.
pub const INPUT_NOTES_COMMITMENT_WORD_IDX: usize = 0;
/// The element index at which the output notes commitment word starts on the output stack.
pub const OUTPUT_NOTES_COMMITMENT_WORD_IDX: usize = 4;
/// The element index at which the batch expiration block number is stored on the output stack.
pub const BATCH_EXPIRATION_BLOCK_NUM_ELEMENT_IDX: usize = 8;

// CONSTRUCTOR
// --------------------------------------------------------------------------------------------

/// Returns a new [`BatchOutput`] instantiated from the provided data.
pub fn new(
input_notes_commitment: Word,
output_notes_commitment: Word,
batch_expiration_block_num: BlockNumber,
) -> Self {
Self {
input_notes_commitment,
output_notes_commitment,
batch_expiration_block_num,
}
}

// PUBLIC ACCESSORS
// --------------------------------------------------------------------------------------------

/// Returns the commitment to the batch's input notes.
pub fn input_notes_commitment(&self) -> Word {
self.input_notes_commitment
}

/// Returns the commitment to the batch's output notes.
pub fn output_notes_commitment(&self) -> Word {
self.output_notes_commitment
}

/// Returns the block number at which the batch expires.
pub fn batch_expiration_block_num(&self) -> BlockNumber {
self.batch_expiration_block_num
}
}
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 @@ -429,6 +429,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