Skip to content

Commit fc480f2

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 5bccee1 commit fc480f2

8 files changed

Lines changed: 123 additions & 17 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: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use miden::core::sys
22

3+
use miden::batch_kernel::memory
34
use miden::batch_kernel::note_tracker
45
use miden::batch_kernel::prologue
56

@@ -11,7 +12,7 @@ use miden::batch_kernel::prologue
1112
#! A transaction batch groups a set of independently-proven transactions so they can later be
1213
#! aggregated into a block by the block kernel. This program reconstructs the per-transaction data
1314
#! from the advice provider, anchoring the reconstruction in the public `BATCH_ID`, and emits the
14-
#! batch's `INPUT_NOTES_COMMITMENT`.
15+
#! batch's `INPUT_NOTES_COMMITMENT` and its effective `batch_expiration_block_num`.
1516
#!
1617
#! Reconstruction is a recursive unhashing chain. Each layer of advice data is keyed by a hash that
1718
#! the previous layer's verification produced, so once `BATCH_ID` is anchored to the public input,
@@ -45,12 +46,14 @@ use miden::batch_kernel::prologue
4546
#! - INPUT_NOTES_COMMITMENT is the sequential hash over every transaction's verified
4647
#! `(NULLIFIER, EMPTY_OR_COMMITMENT)` tuples in transaction order.
4748
#! - BATCH_NOTE_TREE_ROOT is wired up in a follow-up PR; for now it is the empty word.
48-
#! - batch_expiration_block_num is wired up in a follow-up PR; for now it is zero.
49+
#! - batch_expiration_block_num is the minimum of every transaction's `expiration_block_num`.
4950
#!
5051
#! TODO: verify BLOCK_COMMITMENT against block header data via the same pipe-and-verify pattern.
5152
#! TODO: authenticate unauthenticated input notes against BLOCK_COMMITMENT's chain MMR.
5253
#! TODO: erase intra-batch unauthenticated notes from INPUT_NOTES_COMMITMENT.
53-
#! 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`).
5457
#! TODO: aggregate per-account updates and emit a separate ACCOUNT_UPDATES_COMMITMENT output.
5558
#! TODO: recursively verify each transaction's `ExecutionProof`.
5659
proc main
@@ -62,11 +65,23 @@ proc main
6265
# Stack: []
6366

6467
exec.note_tracker::compute_input_notes_commitment
65-
# Stack: [INPUT_NOTES_COMMITMENT]
68+
# Stack: [INPUT_NOTES_COMMITMENT, pad(8)]
6669

67-
# The BATCH_NOTE_TREE_ROOT and batch_expiration_block_num output cells stay the zero padding
68-
# for now (wired up in follow-up PRs). Truncate the operand stack back to the 16-felt output
69-
# region.
70+
# Assemble the output region [INPUT_NOTES_COMMITMENT, BATCH_NOTE_TREE_ROOT, expiration].
71+
# BATCH_NOTE_TREE_ROOT stays the empty word for now (wired up in a follow-up PR).
72+
padw
73+
# Stack: [BATCH_NOTE_TREE_ROOT, INPUT_NOTES_COMMITMENT, pad(8)]
74+
75+
exec.memory::get_batch_expiration_block_num
76+
# Stack: [batch_expiration_block_num, BATCH_NOTE_TREE_ROOT, INPUT_NOTES_COMMITMENT, pad(8)]
77+
78+
movdn.8
79+
# Stack: [BATCH_NOTE_TREE_ROOT, INPUT_NOTES_COMMITMENT, batch_expiration_block_num, pad(8)]
80+
81+
swapw
82+
# Stack: [INPUT_NOTES_COMMITMENT, BATCH_NOTE_TREE_ROOT, batch_expiration_block_num, pad(8)]
83+
84+
# Truncate the operand stack back to the 16-felt output region.
7085
exec.sys::truncate_stack
7186
end
7287

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,10 @@ impl BatchKernel {
152152
/// `TransactionId::input_elements`).
153153
/// - each per-tx `INPUT_NOTES_COMMITMENT` -> the `(NULLIFIER, EMPTY_OR_COMMITMENT)` tuples.
154154
///
155-
/// The per-tx output-notes layer and the expiration data are wired up in follow-up PRs.
155+
/// It also pushes each transaction's `expiration_block_num` onto the advice stack, from which
156+
/// the kernel accumulates the batch-wide running minimum.
157+
///
158+
/// The per-tx output-notes layer is wired up in a follow-up PR.
156159
fn build_advice_inputs(proposed_batch: &ProposedBatch) -> AdviceInputs {
157160
let mut advice_inputs = AdviceInputs::default();
158161

@@ -190,6 +193,13 @@ impl BatchKernel {
190193
}
191194
}
192195

196+
// Advice stack: each transaction's expiration_block_num, in transaction order. The kernel
197+
// pops one per transaction and accumulates the running minimum; order is irrelevant to the
198+
// resulting minimum.
199+
for tx in proposed_batch.transactions().iter() {
200+
advice_inputs.stack.push(Felt::from(tx.expiration_block_num()));
201+
}
202+
193203
advice_inputs
194204
}
195205
}

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
@@ -104,21 +104,26 @@ fn run_kernel(
104104
// ================================================================================================
105105

106106
/// The batch kernel reconstructs every transaction's input notes from the advice provider, anchors
107-
/// them in `BATCH_ID`, and emits the batch's `INPUT_NOTES_COMMITMENT`. The batch note tree root and
108-
/// expiration outputs are not wired up yet, so they remain empty / zero.
107+
/// them in `BATCH_ID`, and emits the batch's `INPUT_NOTES_COMMITMENT` and the running-min
108+
/// `batch_expiration_block_num`. The batch note tree root is not wired up yet, so it remains empty.
109109
#[test]
110-
fn batch_kernel_emits_input_notes_commitment() -> anyhow::Result<()> {
110+
fn batch_kernel_emits_input_notes_commitment_and_expiration() -> anyhow::Result<()> {
111111
let mut setup = setup_chain();
112112
let batch = two_tx_batch(&mut setup)?;
113113
let expected_input_notes_commitment = expected_input_notes_commitment(&batch);
114+
// The expected expiration is the minimum over the batch's transactions, which the proposed
115+
// batch derives independently of the kernel.
116+
let expected_expiration = batch.batch_expiration_block_num();
114117

115118
let executed = BatchExecutor::new().execute(batch).context("batch execution failed")?;
116119
let output = BatchKernel::parse_output_stack(executed.stack_outputs())
117120
.context("parse output stack failed")?;
118121

119122
assert_eq!(output.input_notes_commitment(), expected_input_notes_commitment);
120123
assert_eq!(output.batch_note_tree_root(), Word::empty());
121-
assert_eq!(output.batch_expiration_block_num(), BlockNumber::from(0u32));
124+
assert_eq!(output.batch_expiration_block_num(), expected_expiration);
125+
// Sanity check: the min of the two transactions' expirations (1234 and 800).
126+
assert_eq!(output.batch_expiration_block_num(), BlockNumber::from(800u32));
122127

123128
Ok(())
124129
}

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,21 @@ impl BatchExecutor {
5050
.execute_trace_inputs_sync(&BatchKernel::main(), &mut host)
5151
.map_err(ProvenBatchError::BatchKernelExecutionFailed)?;
5252

53-
// Validate the output stack shape (padding cells are zero and the expiration fits in u32);
54-
// the actual output values themselves are not checked until the kernel verifies them.
55-
BatchKernel::parse_output_stack(trace_inputs.stack_outputs())
53+
// Validate the output stack shape (padding cells are zero and the expiration fits in u32).
54+
let batch_output = BatchKernel::parse_output_stack(trace_inputs.stack_outputs())
5655
.map_err(ProvenBatchError::BatchKernelOutputInvalid)?;
5756

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

0 commit comments

Comments
 (0)