Skip to content

Commit 598251f

Browse files
fix(block-producer): pass through transaction crash (#1396)
1 parent cfe4290 commit 598251f

9 files changed

Lines changed: 510 additions & 132 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22

33
## v0.12.6 (TBD)
44

5+
### Enhancements
6+
57
- Added Faucet metadata to the `miden-network-monitor` binary ([#1373](https://github.com/0xMiden/miden-node/pull/1373)).
68

9+
### Fixes
10+
11+
- Block producer crash caused by pass through transactions ([#1396](https://github.com/0xMiden/miden-node/pull/1396)).
12+
713
## v0.12.5 (2025-11-27)
814

915
- Actually update `miden-base` dependencies ([#1384](https://github.com/0xMiden/miden-node/pull/1384)).

crates/block-producer/src/batch_builder/mod.rs

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use tokio::time;
1717
use tracing::{Instrument, Span, instrument};
1818
use url::Url;
1919

20+
use crate::domain::batch::SelectedBatch;
2021
use crate::domain::transaction::AuthenticatedTransaction;
2122
use crate::errors::BuildBatchError;
2223
use crate::mempool::SharedMempool;
@@ -168,7 +169,7 @@ impl BatchJob {
168169
};
169170

170171
batch.inject_telemetry();
171-
let batch_id = batch.id;
172+
let batch_id = batch.id();
172173

173174
self.get_batch_inputs(batch)
174175
.and_then(|(txs, inputs)| Self::propose_batch(txs, inputs) )
@@ -190,25 +191,21 @@ impl BatchJob {
190191

191192
#[instrument(target = COMPONENT, name = "batch_builder.select_batch", skip_all)]
192193
async fn select_batch(&self) -> Option<SelectedBatch> {
193-
self.mempool
194-
.lock()
195-
.await
196-
.select_batch()
197-
.map(|(id, transactions)| SelectedBatch { id, transactions })
194+
self.mempool.lock().await.select_batch()
198195
}
199196

200197
#[instrument(target = COMPONENT, name = "batch_builder.get_batch_inputs", skip_all, err)]
201198
async fn get_batch_inputs(
202199
&self,
203200
batch: SelectedBatch,
204-
) -> Result<(Vec<Arc<AuthenticatedTransaction>>, BatchInputs), BuildBatchError> {
201+
) -> Result<(SelectedBatch, BatchInputs), BuildBatchError> {
205202
let block_references = batch
206-
.transactions
203+
.txs()
207204
.iter()
208205
.map(Deref::deref)
209206
.map(AuthenticatedTransaction::reference_block);
210207
let unauthenticated_notes = batch
211-
.transactions
208+
.txs()
212209
.iter()
213210
.map(Deref::deref)
214211
.flat_map(AuthenticatedTransaction::unauthenticated_note_commitments);
@@ -217,18 +214,18 @@ impl BatchJob {
217214
.get_batch_inputs(block_references, unauthenticated_notes)
218215
.await
219216
.map_err(BuildBatchError::FetchBatchInputsFailed)
220-
.map(|inputs| (batch.transactions, inputs))
217+
.map(|inputs| (batch, inputs))
221218
}
222219

223220
#[instrument(target = COMPONENT, name = "batch_builder.propose_batch", skip_all, err)]
224221
async fn propose_batch(
225-
transactions: Vec<Arc<AuthenticatedTransaction>>,
222+
selected: SelectedBatch,
226223
inputs: BatchInputs,
227224
) -> Result<ProposedBatch, BuildBatchError> {
228-
let transactions = transactions
229-
.iter()
230-
.map(Deref::deref)
231-
.map(AuthenticatedTransaction::proven_transaction)
225+
let transactions = selected
226+
.into_transactions()
227+
.into_iter()
228+
.map(|tx| tx.proven_transaction())
232229
.collect();
233230

234231
ProposedBatch::new(
@@ -295,11 +292,6 @@ impl BatchJob {
295292
}
296293
}
297294

298-
struct SelectedBatch {
299-
id: BatchId,
300-
transactions: Vec<Arc<AuthenticatedTransaction>>,
301-
}
302-
303295
// BATCH PROVER
304296
// ================================================================================================
305297

@@ -332,11 +324,11 @@ impl BatchProver {
332324

333325
impl TelemetryInjectorExt for SelectedBatch {
334326
fn inject_telemetry(&self) {
335-
Span::current().set_attribute("batch.id", self.id);
336-
Span::current().set_attribute("transactions.count", self.transactions.len());
327+
Span::current().set_attribute("batch.id", self.id());
328+
Span::current().set_attribute("transactions.count", self.txs().len());
337329
// Accumulate all telemetry based on transactions.
338330
let (tx_ids, input_notes_count, output_notes_count, unauth_notes_count) =
339-
self.transactions.iter().fold(
331+
self.txs().iter().fold(
340332
(vec![], 0, 0, 0),
341333
|(
342334
mut tx_ids,
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use std::collections::HashMap;
2+
use std::sync::Arc;
3+
4+
use miden_objects::Word;
5+
use miden_objects::account::AccountId;
6+
use miden_objects::batch::BatchId;
7+
use miden_objects::transaction::TransactionId;
8+
9+
use crate::domain::transaction::AuthenticatedTransaction;
10+
11+
// SELECTED BATCH
12+
// ================================================================================================
13+
14+
/// A sequence of transactions selected by the [`Mempool`] to be processed by the
15+
/// [`BatchBuilder`] into a [`ProposedBatch`], and then finally into a [`ProvenBatch`].
16+
///
17+
/// [Mempool]: crate::mempool::Mempool
18+
/// [BatchBuilder]: crate::batch_builder::BatchBuilder
19+
/// [ProposedBatch]: miden_objects::batch::ProposedBatch
20+
/// [ProvenBatch]: miden_objects::batch::ProvenBatch
21+
#[derive(Clone, Debug, PartialEq)]
22+
pub(crate) struct SelectedBatch {
23+
txs: Vec<Arc<AuthenticatedTransaction>>,
24+
id: BatchId,
25+
account_updates: HashMap<AccountId, (Word, Word)>,
26+
}
27+
28+
impl SelectedBatch {
29+
pub(crate) fn builder() -> SelectedBatchBuilder {
30+
SelectedBatchBuilder::default()
31+
}
32+
33+
pub(crate) fn id(&self) -> BatchId {
34+
self.id
35+
}
36+
37+
pub(crate) fn txs(&self) -> &[Arc<AuthenticatedTransaction>] {
38+
&self.txs
39+
}
40+
41+
pub(crate) fn into_transactions(self) -> Vec<Arc<AuthenticatedTransaction>> {
42+
self.txs
43+
}
44+
45+
/// The aggregated list of account transitions this batch causes given as tuples of `(AccountId,
46+
/// initial commitment, final commitment)`.
47+
///
48+
/// Note that the updates are aggregated, i.e. only a single update per account is possible, and
49+
/// transaction updates to an account of `a -> b -> c` will result in a single `a -> c`.
50+
pub(crate) fn account_updates(&self) -> impl Iterator<Item = (AccountId, Word, Word)> {
51+
self.account_updates.iter().map(|(account, (from, to))| (*account, *from, *to))
52+
}
53+
}
54+
55+
/// A builder to construct a [`SelectedBatch`].
56+
#[derive(Clone, Default)]
57+
pub(crate) struct SelectedBatchBuilder {
58+
pub(crate) txs: Vec<Arc<AuthenticatedTransaction>>,
59+
pub(crate) account_updates: HashMap<AccountId, (Word, Word)>,
60+
}
61+
62+
impl SelectedBatchBuilder {
63+
/// Appends the given transaction to the current batch.
64+
///
65+
/// # Panics
66+
///
67+
/// Panics if the new transaction's account update is inconsistent with the current account
68+
/// state within the batch i.e. if the transaction's initial account commitment does not
69+
/// match the account update's final account commitment within the batch (if any).
70+
pub(crate) fn push(&mut self, tx: Arc<AuthenticatedTransaction>) {
71+
let update = tx.account_update();
72+
self.account_updates
73+
.entry(update.account_id())
74+
.and_modify(|(_, to)| {
75+
assert!(
76+
to == &update.initial_state_commitment(),
77+
"Cannot select transaction {} as its initial commitment {} for account {} does \
78+
not match the current commitment {}",
79+
tx.id(),
80+
update.initial_state_commitment(),
81+
update.account_id(),
82+
to
83+
);
84+
85+
*to = update.final_state_commitment();
86+
})
87+
.or_insert((update.initial_state_commitment(), update.final_state_commitment()));
88+
89+
self.txs.push(tx);
90+
}
91+
92+
/// Returns `true` if the batch contains the given transaction already.
93+
pub(crate) fn contains(&self, target: &TransactionId) -> bool {
94+
self.txs.iter().any(|tx| &tx.id() == target)
95+
}
96+
97+
/// Returns `true` if it contains no transactions.
98+
pub(crate) fn is_empty(&self) -> bool {
99+
self.txs.is_empty()
100+
}
101+
102+
/// Finalizes the batch selection.
103+
pub(crate) fn build(self) -> SelectedBatch {
104+
let Self { txs, account_updates } = self;
105+
let id = BatchId::from_ids(txs.iter().map(|tx| (tx.id(), tx.account_id())));
106+
107+
SelectedBatch { txs, id, account_updates }
108+
}
109+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
pub mod transaction;
1+
pub(crate) mod batch;
2+
pub(crate) mod transaction;

crates/block-producer/src/mempool/mod.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ use subscription::SubscriptionProvider;
5353
use tokio::sync::{Mutex, MutexGuard, mpsc};
5454
use tracing::{instrument, warn};
5555

56+
use crate::domain::batch::SelectedBatch;
5657
use crate::domain::transaction::AuthenticatedTransaction;
5758
use crate::errors::{AddTransactionError, VerifyTxError};
5859
use crate::mempool::budget::BudgetStatus;
@@ -224,7 +225,7 @@ impl Mempool {
224225
///
225226
/// Returns `None` if no transactions are available.
226227
#[instrument(target = COMPONENT, name = "mempool.select_batch", skip_all)]
227-
pub fn select_batch(&mut self) -> Option<(BatchId, Vec<Arc<AuthenticatedTransaction>>)> {
228+
pub fn select_batch(&mut self) -> Option<SelectedBatch> {
228229
// The selection algorithm is fairly neanderthal in nature.
229230
//
230231
// We iterate over all transaction nodes, each time selecting the first transaction which
@@ -239,13 +240,13 @@ impl Mempool {
239240
//
240241
// The additional bookkeeping can be implemented once we have fee related strategies. KISS.
241242

242-
let mut selected = ProposedBatchNode::default();
243+
let mut selected = SelectedBatch::builder();
243244
let mut budget = self.config.batch_budget;
244245

245246
let mut candidates = self.nodes.txs.values();
246247

247248
'next: while let Some(candidate) = candidates.next() {
248-
if selected.contains(candidate.id()) {
249+
if selected.contains(&candidate.id()) {
249250
continue 'next;
250251
}
251252

@@ -255,7 +256,9 @@ impl Mempool {
255256
match parent {
256257
// TODO(mirko): Once user batches are supported, they will also need to be
257258
// checked here.
258-
NodeId::Transaction(parent) if !selected.contains(parent) => continue 'next,
259+
NodeId::Transaction(parent) if !selected.contains(&parent) => {
260+
continue 'next;
261+
},
259262
NodeId::Transaction(_)
260263
| NodeId::ProposedBatch(_)
261264
| NodeId::ProvenBatch(_)
@@ -274,11 +277,12 @@ impl Mempool {
274277
if selected.is_empty() {
275278
return None;
276279
}
280+
let selected = selected.build();
277281

278-
let batch_id = selected.calculate_id();
279-
let batch_txs = selected.transactions().cloned().collect::<Vec<_>>();
282+
let batch = ProposedBatchNode::new(selected.clone());
283+
let batch_id = batch.batch_id();
280284

281-
for tx in &batch_txs {
285+
for tx in batch.transactions() {
282286
let node =
283287
self.nodes.txs.remove(&tx.id()).expect("selected transaction node must exist");
284288
self.state.remove(&node);
@@ -288,13 +292,13 @@ impl Mempool {
288292
"Transaction selected for inclusion in batch"
289293
);
290294
}
291-
self.state.insert(NodeId::ProposedBatch(batch_id), &selected);
292-
self.nodes.proposed_batches.insert(batch_id, selected);
295+
self.state.insert(NodeId::ProposedBatch(batch_id), &batch);
296+
self.nodes.proposed_batches.insert(batch_id, batch);
293297

294298
// TODO(mirko): Selecting a batch can unblock user batches, which should be checked here.
295299

296300
self.inject_telemetry();
297-
Some((batch_id, batch_txs))
301+
Some(selected)
298302
}
299303

300304
/// Drops the proposed batch and all of its descendants.
@@ -376,7 +380,8 @@ impl Mempool {
376380
self.nodes.proposed_block.as_ref().unwrap().0
377381
);
378382

379-
let mut selected = BlockNode::default();
383+
let block_number = self.chain_tip.child();
384+
let mut selected = BlockNode::new(block_number);
380385
let mut budget = self.config.block_budget;
381386
let mut candidates = self.nodes.proven_batches.values();
382387

@@ -404,7 +409,6 @@ impl Mempool {
404409
selected.push(candidate.clone());
405410
}
406411

407-
let block_number = self.chain_tip.child();
408412
// Replace the batches with the block in state and nodes.
409413
for batch in selected.batches() {
410414
// SAFETY: Selected batches came from nodes, and are unique.

0 commit comments

Comments
 (0)