Skip to content

Commit af57214

Browse files
committed
feat: emit batch_expiration_block_num as the running-min over transactions
The batch kernel accumulates the minimum expiration_block_num across all transactions (popped per-tx from the advice stack during Layer 2 header verification) and emits it as the batch_expiration_block_num output. BatchExecutor cross-checks the kernel's value against the expiration the ProposedBatch independently derives, erroring with BatchExpirationMismatch on disagreement.
1 parent 59c2efb commit af57214

8 files changed

Lines changed: 127 additions & 18 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Changes
66
- 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.
77
- The batch kernel now verifies the batch's transaction list against its `BATCH_ID`, verifies each transaction header, and computes the batch `INPUT_NOTES_COMMITMENT` ([#2905](https://github.com/0xMiden/protocol/pull/2905)).
8+
- The batch kernel now emits `batch_expiration_block_num`, the running minimum of its transactions' expiration block numbers, cross-checked against the proposed batch ([#3019](https://github.com/0xMiden/protocol/pull/3019)).
89

910
- [BREAKING] Renamed `AccountStorageDelta` to `AccountStoragePatch` ([#3002](https://github.com/0xMiden/protocol/pull/3002)).
1011
- [BREAKING] Extracted `NullifierTreeBackendReader` and `AccountTreeBackendReader` traits from existing `NullifierTreeBackend` and `AccountTreeBackend` traits ([#2755](https://github.com/0xMiden/protocol/pull/2755)).

‎crates/miden-protocol/asm/kernels/batch/lib/memory.masm‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
# | Address range | Constant | Contents |
88
# +-------------------+-------------------------+----------------------------------+
99
# | 0 | NUM_TRANSACTIONS_PTR | num_transactions (1 felt). |
10+
# | 1 | BATCH_EXPIRATION_PTR | batch_expiration_block_num, |
11+
# | | | the running min over all txs. |
1012
# | 4..8 | BATCH_HASHER_RATE0_PTR | RATE0 of the batch-level |
1113
# | | | poseidon2 hasher state. |
1214
# | 8..12 | BATCH_HASHER_RATE1_PTR | RATE1 of the batch-level hasher. |
@@ -39,6 +41,10 @@
3941
#! Single-felt slot holding `num_transactions` after Layer 1 verification.
4042
const NUM_TRANSACTIONS_PTR=0
4143

44+
#! Single-felt slot holding `batch_expiration_block_num`: the running minimum of every
45+
#! transaction's `expiration_block_num`, initialised to `u32::MAX`.
46+
const BATCH_EXPIRATION_PTR=1
47+
4248
# BATCH HASHER STATE
4349
# =================================================================================================
4450

@@ -107,6 +113,25 @@ pub proc get_num_transactions
107113
mem_load.NUM_TRANSACTIONS_PTR
108114
end
109115

116+
# BATCH EXPIRATION BLOCK NUM
117+
# =================================================================================================
118+
119+
#! Stores `batch_expiration_block_num`.
120+
#!
121+
#! Inputs: [batch_expiration_block_num]
122+
#! Outputs: []
123+
pub proc set_batch_expiration_block_num
124+
mem_store.BATCH_EXPIRATION_PTR
125+
end
126+
127+
#! Returns `batch_expiration_block_num`.
128+
#!
129+
#! Inputs: []
130+
#! Outputs: [batch_expiration_block_num]
131+
pub proc get_batch_expiration_block_num
132+
mem_load.BATCH_EXPIRATION_PTR
133+
end
134+
110135
# BATCH HASHER STATE
111136
# =================================================================================================
112137

‎crates/miden-protocol/asm/kernels/batch/lib/prologue.masm‎

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ use miden::core::mem
33
use miden::batch_kernel::memory
44
use miden::batch_kernel::memory::TX_TUPLES_PTR
55

6+
# CONSTANTS
7+
# =================================================================================================
8+
9+
#! Maximum value of `batch_expiration_block_num`, used as the running-min initialiser.
10+
const MAX_BLOCK_NUM=0xFFFFFFFF
11+
612
# PROLOGUE
713
# =================================================================================================
814

@@ -16,7 +22,8 @@ use miden::batch_kernel::memory::TX_TUPLES_PTR
1622
#! - Layer 2: for each transaction, pipes the felt sequence hashed by `TransactionId::new` from the
1723
#! advice map keyed by the verified `tx_id`, asserting that the sequential hash matches the
1824
#! `tx_id`. Each transaction's data is written into [`memory::TX_HEADERS_PTR`] at the appropriate
19-
#! per-tx offset.
25+
#! per-tx offset. For each transaction, pops its `expiration_block_num` from the advice stack and
26+
#! updates the running minimum in [`memory::BATCH_EXPIRATION_PTR`].
2027
#!
2128
#! `BATCH_ID` is the batch's `BatchId` value, i.e. the sequential hash of the `(tx_id, account_id)`
2229
#! tuples committing to the transactions in the batch.
@@ -28,6 +35,7 @@ use miden::batch_kernel::memory::TX_TUPLES_PTR
2835
#! For each verified tx_id_i:
2936
#! tx_id_i |-> [INIT_i, FINAL_i, INPUT_NOTES_COMMITMENT_i, OUTPUT_NOTES_COMMITMENT_i,
3037
#! FEE_ASSET_i]
38+
#! Advice stack: [expiration_block_num_0, expiration_block_num_1, ...]
3139
#!
3240
#! Outputs:
3341
#! Operand stack: []
@@ -40,6 +48,9 @@ use miden::batch_kernel::memory::TX_TUPLES_PTR
4048
#! TODO: verify that each transaction's reference block is contained in the chain MMR rooted at
4149
#! BLOCK_COMMITMENT.
4250
#! TODO: verify that the partial-blockchain peaks hash matches the block header's chain commitment.
51+
#! TODO: assert each `expiration_block_num_i > reference_block_num`.
52+
#! TODO: derive each `expiration_block_num_i` from data committed-to in the verified transaction
53+
#! header rather than from the unverified advice stack.
4354
pub proc prepare_batch
4455
# Layer 1: pipe BATCH_ID's mapped value to tx_tuples_ptr + verify.
4556
# ---------------------------------------------------------------------------------------------
@@ -65,7 +76,12 @@ pub proc prepare_batch
6576
# Stack: [end_ptr]
6677
drop
6778

68-
# Layer 2: for each transaction, pipe + verify its header.
79+
# Initialise batch_expiration_block_num to u32::MAX.
80+
# ---------------------------------------------------------------------------------------------
81+
82+
push.MAX_BLOCK_NUM exec.memory::set_batch_expiration_block_num
83+
84+
# Layer 2: for each transaction, pipe + verify its header, accumulate expiration min.
6985
# ---------------------------------------------------------------------------------------------
7086

7187
exec.memory::get_num_transactions
@@ -94,6 +110,23 @@ pub proc prepare_batch
94110
# Stack: [end_ptr, tx_index, num_transactions]
95111
drop
96112

113+
# Accumulate the expiration running-min from the advice stack.
114+
adv_push
115+
# Stack: [expiration, tx_index, num_transactions]
116+
117+
dup exec.memory::get_batch_expiration_block_num
118+
# Stack: [current_min, expiration, expiration, tx_index, num_transactions]
119+
120+
u32lt
121+
# Stack: [is_new_min, expiration, tx_index, num_transactions]
122+
123+
if.true
124+
exec.memory::set_batch_expiration_block_num
125+
else
126+
drop
127+
end
128+
# Stack: [tx_index, num_transactions]
129+
97130
add.1
98131
dup.1 dup.1 neq
99132
# Stack: [should_loop, tx_index, num_transactions]

‎crates/miden-protocol/asm/kernels/batch/main.masm‎

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use miden::core::sys
2+
3+
use miden::batch_kernel::memory
14
use miden::batch_kernel::note_tracker
25
use miden::batch_kernel::prologue
36

@@ -9,7 +12,7 @@ use miden::batch_kernel::prologue
912
#! A transaction batch groups a set of independently-proven transactions so they can later be
1013
#! aggregated into a block by the block kernel. This program reconstructs the per-transaction data
1114
#! from the advice provider, anchoring the reconstruction in the public `BATCH_ID`, and emits the
12-
#! batch's `INPUT_NOTES_COMMITMENT`.
15+
#! batch's `INPUT_NOTES_COMMITMENT` and its effective `batch_expiration_block_num`.
1316
#!
1417
#! Reconstruction is a recursive unhashing chain. Each layer of advice data is keyed by a hash that
1518
#! the previous layer's verification produced, so once `BATCH_ID` is anchored to the public input,
@@ -43,12 +46,14 @@ use miden::batch_kernel::prologue
4346
#! - INPUT_NOTES_COMMITMENT is the sequential hash over every transaction's
4447
#! `(NULLIFIER, EMPTY_OR_COMMITMENT)` tuples.
4548
#! - BATCH_NOTE_TREE_ROOT is currently not wired up (empty word).
46-
#! - batch_expiration_block_num is currently not wired-up (zero).
49+
#! - batch_expiration_block_num is the minimum of every transaction's `expiration_block_num`.
4750
#!
4851
#! TODO: verify BLOCK_COMMITMENT against block header data via the same pipe-and-verify pattern.
4952
#! TODO: authenticate unauthenticated input notes against BLOCK_COMMITMENT's chain MMR.
5053
#! TODO: erase intra-batch unauthenticated notes from INPUT_NOTES_COMMITMENT.
51-
#! TODO: emit BATCH_NOTE_TREE_ROOT (the batch note tree SMT root) and batch_expiration_block_num.
54+
#! TODO: emit BATCH_NOTE_TREE_ROOT (the batch note tree SMT root).
55+
#! TODO: derive batch_expiration_block_num from verified data rather than the unverified advice
56+
#! stack (see `prologue::prepare_batch`).
5257
#! TODO: aggregate per-account updates and emit a separate ACCOUNT_UPDATES_COMMITMENT output.
5358
#! TODO: recursively verify each transaction's `ExecutionProof`.
5459
proc main
@@ -64,12 +69,25 @@ proc main
6469
exec.note_tracker::compute_input_notes_commitment
6570
# Stack: [INPUT_NOTES_COMMITMENT, pad(16)]
6671

67-
# Drop the surplus padding word so the stack holds exactly the 16-felt output region
68-
# [INPUT_NOTES_COMMITMENT, BATCH_NOTE_TREE_ROOT, batch_expiration_block_num, pad(7)]. The
69-
# BATCH_NOTE_TREE_ROOT and batch_expiration_block_num cells stay zero (wired up in follow-up
70-
# PRs).
71-
movupw.3 dropw
72-
# Stack: [INPUT_NOTES_COMMITMENT, pad(12)]
72+
# Assemble the output region [INPUT_NOTES_COMMITMENT, BATCH_NOTE_TREE_ROOT, expiration]. The
73+
# BATCH_NOTE_TREE_ROOT word stays empty for now (wired up in a follow-up PR).
74+
padw
75+
# Stack: [BATCH_NOTE_TREE_ROOT, INPUT_NOTES_COMMITMENT, pad(16)]
76+
77+
exec.memory::get_batch_expiration_block_num
78+
# Stack: [batch_expiration_block_num, BATCH_NOTE_TREE_ROOT, INPUT_NOTES_COMMITMENT, pad(16)]
79+
80+
movdn.8
81+
# Stack: [BATCH_NOTE_TREE_ROOT, INPUT_NOTES_COMMITMENT, batch_expiration_block_num, pad(16)]
82+
83+
swapw
84+
# Stack: [INPUT_NOTES_COMMITMENT, BATCH_NOTE_TREE_ROOT, batch_expiration_block_num, pad(16)]
85+
86+
# batch_expiration_block_num is a single felt, so the output region (9 felts) is not
87+
# word-aligned and the surplus padding cannot be dropped in whole words; truncate the operand
88+
# stack to the 16-felt output [INPUT_NOTES_COMMITMENT, BATCH_NOTE_TREE_ROOT,
89+
# batch_expiration_block_num, pad(7)].
90+
exec.sys::truncate_stack
7391
end
7492

7593
begin

‎crates/miden-protocol/src/batch/kernel.rs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ impl BatchKernel {
111111
/// `TransactionId::input_elements`).
112112
/// - each per-tx `INPUT_NOTES_COMMITMENT` -> the `(NULLIFIER, EMPTY_OR_COMMITMENT)` tuples.
113113
///
114-
/// The per-tx output-notes layer and the expiration data are wired up in follow-up PRs.
114+
/// It also pushes each transaction's `expiration_block_num` onto the advice stack, from which
115+
/// the kernel accumulates the batch-wide running minimum.
116+
///
117+
/// The per-tx output-notes layer is wired up in a follow-up PR.
115118
fn build_advice_inputs(proposed_batch: &ProposedBatch) -> AdviceInputs {
116119
let mut advice_inputs = AdviceInputs::default();
117120

@@ -153,6 +156,13 @@ impl BatchKernel {
153156
}
154157
}
155158

159+
// Advice stack: each transaction's expiration_block_num, in transaction order. The kernel
160+
// pops one per transaction and accumulates the running minimum; order is irrelevant to the
161+
// resulting minimum.
162+
for tx in proposed_batch.transactions().iter() {
163+
advice_inputs.stack.push(Felt::from(tx.expiration_block_num()));
164+
}
165+
156166
advice_inputs
157167
}
158168
}

‎crates/miden-protocol/src/errors/mod.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,13 @@ pub enum ProvenBatchError {
10961096
BatchKernelExecutionFailed(#[source] ExecutionError),
10971097
#[error("batch kernel produced an invalid output stack")]
10981098
BatchKernelOutputInvalid(#[source] BatchOutputError),
1099+
#[error(
1100+
"batch kernel computed expiration block number {actual} but the proposed batch expected {expected}"
1101+
)]
1102+
BatchExpirationMismatch {
1103+
actual: BlockNumber,
1104+
expected: BlockNumber,
1105+
},
10991106
}
11001107

11011108
// BATCH OUTPUT ERROR

‎crates/miden-testing/src/kernel_tests/batch/test_batch_kernel.rs‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,20 +113,25 @@ fn run_kernel(
113113
// ================================================================================================
114114

115115
/// The batch kernel reconstructs every transaction's input notes from the advice provider, anchors
116-
/// them in `BATCH_ID`, and emits the batch's `INPUT_NOTES_COMMITMENT`. The batch note tree root and
117-
/// expiration outputs are not wired up yet, so they remain empty / zero.
116+
/// them in `BATCH_ID`, and emits the batch's `INPUT_NOTES_COMMITMENT` and the running-min
117+
/// `batch_expiration_block_num`. The batch note tree root is not wired up yet, so it remains empty.
118118
#[test]
119-
fn batch_kernel_emits_input_notes_commitment() -> anyhow::Result<()> {
119+
fn batch_kernel_emits_input_notes_commitment_and_expiration() -> anyhow::Result<()> {
120120
let mut setup = setup_chain();
121121
let batch = two_tx_batch(&mut setup)?;
122122
let expected_input_notes_commitment = expected_input_notes_commitment(&batch);
123+
// The expected expiration is the minimum over the batch's transactions, which the proposed
124+
// batch derives independently of the kernel.
125+
let expected_expiration = batch.batch_expiration_block_num();
123126

124127
let executed = BatchExecutor::new().execute(batch).context("batch execution failed")?;
125128
let output = executed.batch_outputs();
126129

127130
assert_eq!(output.input_notes_commitment(), expected_input_notes_commitment);
128131
assert_eq!(output.batch_note_tree_root(), Word::empty());
129-
assert_eq!(output.batch_expiration_block_num(), BlockNumber::from(0u32));
132+
assert_eq!(output.batch_expiration_block_num(), expected_expiration);
133+
// Sanity check: the min of the two transactions' expirations (1234 and 800).
134+
assert_eq!(output.batch_expiration_block_num(), BlockNumber::from(800u32));
130135

131136
Ok(())
132137
}

‎crates/miden-tx-batch-prover/src/batch_executor.rs‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,21 @@ impl BatchExecutor {
5151
.map_err(ProvenBatchError::BatchKernelExecutionFailed)?;
5252

5353
// Parse and validate the output stack shape (padding cells are zero and the expiration
54-
// fits in u32); the actual output values themselves are not checked until the kernel
55-
// verifies them.
54+
// fits in u32).
5655
let batch_outputs = BatchOutputs::parse(trace_inputs.stack_outputs())
5756
.map_err(ProvenBatchError::BatchKernelOutputInvalid)?;
5857

58+
// Cross-check the kernel's running-min expiration against the value the proposed batch
59+
// independently derived from its transactions.
60+
let expected_expiration = proposed_batch.batch_expiration_block_num();
61+
let actual_expiration = batch_outputs.batch_expiration_block_num();
62+
if actual_expiration != expected_expiration {
63+
return Err(ProvenBatchError::BatchExpirationMismatch {
64+
actual: actual_expiration,
65+
expected: expected_expiration,
66+
});
67+
}
68+
5969
Ok(ExecutedBatch::new(proposed_batch, trace_inputs, batch_outputs))
6070
}
6171
}

0 commit comments

Comments
 (0)