Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions tests/fixtures/batch-kernel/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This fixture is intended to be built as Wasm for the Miden VM.

[build]
target = "wasm32-wasip2"

[target.wasm32-wasip2]
# Force-enable `cfg(miden)` for Miden-VM-targeted builds (including editor/LSP workflows).
rustflags = ["--cfg", "miden"]
1 change: 1 addition & 0 deletions tests/fixtures/batch-kernel/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
22 changes: 22 additions & 0 deletions tests/fixtures/batch-kernel/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
cargo-features = ["trim-paths"]

[package]
name = "batch-kernel"
version = "0.1.0"
edition = "2024"

[lib]
# Build this crate as a self-contained, C-style dynamic library
# This is required to emit the proper Wasm module type
crate-type = ["cdylib"]

[dependencies]
miden-sdk-alloc = { path = "../../../sdk/alloc" }
miden-stdlib-sys = { path = "../../../sdk/stdlib-sys" }

[profile.release]
panic = "abort"
# optimize for size
opt-level = "z"
debug = false
trim-paths = ["diagnostics", "object"]
10 changes: 10 additions & 0 deletions tests/fixtures/batch-kernel/miden-project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "batch-kernel"
version = "0.1.0"

[[bin]]
name = "batch-kernel"
path = "src/lib.rs"

[dependencies]
miden-core = "*"
131 changes: 131 additions & 0 deletions tests/fixtures/batch-kernel/src/epilogue.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//! Batch kernel epilogue: verifies the note-tracking results and computes the batch's note
//! commitments.
//!
//! Mirrors `asm/kernels/batch/lib/epilogue.masm` from the protocol batch kernel
//! (0xMiden/protocol#2905).

extern crate alloc;
use alloc::vec::Vec;

use miden_stdlib_sys::{Felt, Word, felt, hash_elements};

use crate::memory::{
BatchMemory, INPUT_FLAGS_STRIDE, NOTE_ENTRY_FELT_LEN, OUTPUT_FLAGS_STRIDE, erasure_erased,
erasure_expected,
};

// ASSERTIONS
// =================================================================================================

/// Asserts every output-note list entry was created by exactly one transaction (with the per-tx
/// binding, this proves the list is exactly the union of the per-transaction output notes, so a
/// host cannot fabricate an erasure with a note no transaction creates).
///
/// The matching input-note checks (every entry consumed exactly once, none left pending-erasure)
/// are folded into the single pass in [`compute_input_notes_commitment`].
#[inline(always)]
fn assert_all_output_notes_created(memory: &BatchMemory) {
for flags in memory.output_note_flags().chunks_exact(OUTPUT_FLAGS_STRIDE) {
assert!(
flags[1] != felt!(0),
"an output-note list entry was not created by any transaction"
);
}
}

// INPUT NOTES COMMITMENT
// =================================================================================================

/// Computes `INPUT_NOTES_COMMITMENT` as the sequential hash of the non-erased
/// `(NULLIFIER, NOTE_ID_OR_EMPTY)` entries of the nullifier-sorted input-note list (entries with
/// erasure flag `Erased` are skipped).
///
/// The pass over the input entries also enforces the epilogue invariants on each entry: it was
/// consumed exactly once and is not left expected-to-be-erased (i.e. any created-and-consumed
/// note had its creator processed).
///
/// The MASM kernel absorbs the entries into an incremental hasher state persisted in memory;
/// here the sorted note list buffer is handed to `hash_elements` whole in the common no-erasure
/// case (which is why this takes `BatchMemory` by value), and only batches with erased notes
/// collect the surviving entries into a fresh buffer first. Both produce the same sequential
/// hash (matching `Hasher::hash_elements`).
#[inline(always)]
fn compute_input_notes_commitment(memory: BatchMemory) -> Word {
let num_notes = memory.num_input_notes();
let mut num_erased = 0;

for flags in memory.input_note_flags().chunks_exact(INPUT_FLAGS_STRIDE) {
// Assert this entry was consumed exactly once and is not left expected-to-be-erased.
assert!(
flags[1] != felt!(0),
"an input-note list entry was not consumed by any transaction"
);
let erasure = flags[0];
assert!(
erasure != erasure_expected(),
"an erased input note was consumed before the transaction that creates it"
);
if erasure == erasure_erased() {
num_erased += 1;
}
}

// With no non-erased entries the commitment is the empty word, matching the early return in
// `build_input_note_commitment` (this is not the hash of zero elements).
if num_erased == num_notes {
return Word::empty();
}

// Common case: nothing erased, hash the sorted note list buffer as-is.
if num_erased == 0 {
return Word::from(hash_elements(memory.input_notes));
}

// Some entries were erased: collect the surviving entries and hash those.
let mut elements: Vec<Felt> =
Vec::with_capacity((num_notes - num_erased) * NOTE_ENTRY_FELT_LEN);
for (entry, flags) in memory
.input_notes
.chunks_exact(NOTE_ENTRY_FELT_LEN)
.zip(memory.input_note_flags().chunks_exact(INPUT_FLAGS_STRIDE))
{
if flags[0] != erasure_erased() {
elements.extend_from_slice(entry);
}
}
Word::from(hash_elements(elements))
}

// OUTPUT NOTES COMMITMENT
// =================================================================================================

/// Computes the batch's output-notes commitment (the batch note tree root).
///
/// Placeholder: returns the empty word until the batch note tree is wired up.
///
/// TODO: hash the batch's output notes into the batch note tree (SMT) root.
#[inline(always)]
fn compute_output_notes_commitment(_memory: &BatchMemory) -> Word {
Word::empty()
}

// EPILOGUE
// =================================================================================================

/// Verifies the note-tracking results and computes the batch's note commitments, returned as
/// `(INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT)`. Consumes the batch state (the input-note
/// buffer becomes the hash input in the no-erasure case).
///
/// Asserts every output-note list entry was created, then computes the input- and output-notes
/// commitments. The per-input-entry invariants (consumed exactly once, no pending erasure) are
/// enforced inside the input-commitment pass.
///
/// TODO: authenticate unauthenticated, non-erased input notes against BLOCK_COMMITMENT's chain
/// MMR.
#[inline(always)]
pub fn finalize(memory: BatchMemory) -> (Word, Word) {
assert_all_output_notes_created(&memory);
let output_notes_commitment = compute_output_notes_commitment(&memory);
let input_notes_commitment = compute_input_notes_commitment(memory);
(input_notes_commitment, output_notes_commitment)
}
118 changes: 118 additions & 0 deletions tests/fixtures/batch-kernel/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! Rust implementation of the Miden protocol batch kernel, compiled to Miden Assembly by the
//! Miden compiler.
//!
//! 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 validates the per-transaction data
//! supplied via the advice provider, and computes the batch's `INPUT_NOTES_COMMITMENT` and its
//! effective `batch_expiration_block_num`.
//!
//! The implementation mirrors the MASM batch kernel of the protocol repository — the phase
//! structure and checks follow `asm/kernels/batch/{main.masm,lib/*.masm}` of
//! 0xMiden/protocol#2905 (with the expiration running-minimum of 0xMiden/protocol#3019):
//!
//! - [`prologue`]: "unhashes" the layered advice data anchored at the public `BATCH_ID`. Each
//! layer of advice data is keyed by a hash the previous layer verified, so every element that
//! makes up `INPUT_NOTES_COMMITMENT` is transitively committed-to:
//!
//! - `BATCH_ID` (public input) -> `(tx_id, account_id)` tuple list
//! - each `tx_id` -> per-tx `(INIT, FINAL, INPUT_NOTES_COMMITMENT_i,
//! OUTPUT_NOTES_COMMITMENT_i)`
//! - each `INPUT_NOTES_COMMITMENT_i` -> `(NULLIFIER, NOTE_ID_OR_EMPTY)` tuples
//!
//! - [`note_tracker`]: determines intra-batch note erasure and binds the host-provided sorted
//! note lists to the verified per-transaction notes.
//! - [`epilogue`]: enforces the tracking invariants and computes the output commitments.
//!
//! # Inputs
//!
//! - operand stack: `BLOCK_COMMITMENT` and `BATCH_ID`, one felt per parameter (the MASM kernel's
//! `[BLOCK_COMMITMENT, BATCH_ID, pad(8)]` public inputs), plus the output pointer.
//! - advice map and stack: see [`prologue::prepare_batch`].
//!
//! # Outputs
//!
//! The MASM kernel's output stack is `[INPUT_NOTES_COMMITMENT, BATCH_NOTE_TREE_ROOT,
//! batch_expiration_block_num, pad(7)]`. Here the two commitment words are written to `out_ptr`
//! and `batch_expiration_block_num` is the return value:
//!
//! - `INPUT_NOTES_COMMITMENT` is the nullifier-sorted sequential hash over the batch's input
//! notes, excluding notes created and consumed within the batch (i.e. post-erasure).
//! - `BATCH_NOTE_TREE_ROOT` is emitted as the empty word (not yet computed).
//! - `batch_expiration_block_num` is the minimum of every transaction's `expiration_block_num`.
//!
//! TODO: verify BLOCK_COMMITMENT against block header data.
//! TODO: authenticate unauthenticated, non-erased input notes against BLOCK_COMMITMENT's chain
//! MMR.
//! TODO: emit BATCH_NOTE_TREE_ROOT (the batch note tree SMT root).
//! TODO: aggregate per-account updates and emit a separate ACCOUNT_UPDATES_COMMITMENT output.
//! TODO: recursively verify each transaction's `ExecutionProof`.

#![no_std]
#![no_main]
#![feature(alloc_error_handler)]

extern crate alloc;

#[global_allocator]
static ALLOC: miden_sdk_alloc::BumpAlloc = miden_sdk_alloc::BumpAlloc::new();

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
core::arch::wasm32::unreachable()
}

#[alloc_error_handler]
fn alloc_error(_layout: core::alloc::Layout) -> ! {
core::arch::wasm32::unreachable()
}

mod epilogue;
mod memory;
mod note_tracker;
mod prologue;

use miden_stdlib_sys::{Felt, Word};

/// Batch kernel program.
///
/// See the crate documentation for the input/output contract.
// The entrypoint is invoked by the VM with its arguments on the operand stack, so `unsafe fn`
// would not communicate anything to its caller; `out_ptr` validity is the executing host's
// responsibility.
#[allow(clippy::not_unsafe_ptr_arg_deref)]
#[unsafe(no_mangle)]
#[allow(improper_ctypes_definitions)]
pub extern "C" fn entrypoint(
out_ptr: *mut Felt,
_block_commitment0: Felt,
_block_commitment1: Felt,
_block_commitment2: Felt,
_block_commitment3: Felt,
batch_id0: Felt,
batch_id1: Felt,
batch_id2: Felt,
batch_id3: Felt,
) -> Felt {
// TODO: verify BLOCK_COMMITMENT against block header data via the pipe-and-verify pattern
// (the MASM kernel drops it the same way).
let batch_id = Word::from([batch_id0, batch_id1, batch_id2, batch_id3]);

let mut memory = prologue::prepare_batch(batch_id);

note_tracker::track_notes(&mut memory);

let batch_expiration_block_num = memory.batch_expiration_block_num;
let (input_notes_commitment, batch_note_tree_root) = epilogue::finalize(memory);

// Lay the output words out at `out_ptr`: [INPUT_NOTES_COMMITMENT, BATCH_NOTE_TREE_ROOT].
for (offset, felt) in input_notes_commitment
.as_elements()
.iter()
.chain(batch_note_tree_root.as_elements().iter())
.enumerate()
{
unsafe { out_ptr.add(offset).write(*felt) };
}

batch_expiration_block_num
}
Loading
Loading