Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- [BREAKING] Removed `miden-node ntx-builder` subcommand and created a separate `miden-ntx-builder` binary ([#2067](https://github.com/0xMiden/node/pull/2067)).
- [BREAKING] Reworked note proto types for multi-attachment support: `NoteMetadata` now carries `attachment_schemes` (repeated) and `attachments_commitment` instead of a single `attachment`. `Note` and `NetworkNote` gained an `attachments` field. `NoteSyncRecord` now embeds full `NoteMetadata` instead of `NoteMetadataHeader`. Removed `NoteAttachmentKind` enum and `NoteMetadataHeader` message ([#2078](https://github.com/0xMiden/node/pull/2078)).
- [BREAKING] Changed `SyncChainMmr` endpoint: the upper end of the block range we're syncing is now the chain tip with the requested finality level. Validator signature is also returned ([#2075](https://github.com/0xMiden/node/pull/2075)).
- Fixed block producer mempool panic when selecting transactions that depend on notes created by pruned committed transactions ([#2097](https://github.com/0xMiden/node/pull/2097)).

## v0.14.10 (2026-05-29)

Expand Down
9 changes: 7 additions & 2 deletions crates/block-producer/src/domain/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ pub struct AuthenticatedTransaction {
/// This does not necessarily have to match the transaction's initial state
/// as this may still be modified by inflight transactions.
store_account_state: Option<Word>,
/// Unauthenticated note commitments that have now been authenticated by the store
/// [inputs](TransactionInputs).
/// Unauthenticated note commitments that have now been authenticated by committed state,
/// either through store [inputs](TransactionInputs) or through locally committed mempool
/// history.
///
/// In other words, notes which were unauthenticated at the time the transaction was proven,
/// but which have since been committed to, and authenticated by the store.
Expand Down Expand Up @@ -121,6 +122,10 @@ impl AuthenticatedTransaction {
.filter(|commitment| !self.notes_authenticated_by_store.contains(commitment))
}

pub(crate) fn mark_notes_authenticated(&mut self, notes: impl IntoIterator<Item = Word>) {
self.notes_authenticated_by_store.extend(notes);
}

pub fn proven_transaction(&self) -> Arc<ProvenTransaction> {
Arc::clone(&self.inner)
}
Expand Down
6 changes: 5 additions & 1 deletion crates/block-producer/src/mempool/graph/dag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ where
/// Returns the node and its descendants.
///
/// That is, this returns the node's children, their children etc.
fn descendants(&self, node: &N::Id) -> HashSet<N::Id> {
pub(super) fn descendants(&self, node: &N::Id) -> HashSet<N::Id> {
let mut to_process = vec![*node];
let mut descendants = HashSet::default();

Expand Down Expand Up @@ -274,6 +274,10 @@ where
pub fn contains(&self, node: &N::Id) -> bool {
self.nodes.contains_key(node)
}

pub(super) fn get_mut(&mut self, node: &N::Id) -> Option<&mut N> {
self.nodes.get_mut(node)
}
}

// GRAPH DAG TESTS
Expand Down
19 changes: 19 additions & 0 deletions crates/block-producer/src/mempool/graph/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,13 +320,32 @@ impl TransactionGraph {
/// graph.
pub fn prune(&mut self, batch: &SelectedBatch) {
for tx in batch.transactions() {
self.mark_committed_notes_authenticated_for_descendants(tx);
self.inner.prune(tx.id());
self.failures.remove(&tx.id());
self.txs_user_batch.remove(&tx.id());
}
self.user_batch_txs.remove(&batch.id());
}

fn mark_committed_notes_authenticated_for_descendants(
&mut self,
tx: &Arc<AuthenticatedTransaction>,
) {
let output_notes = tx.output_note_commitments().collect::<HashSet<_>>();
if output_notes.is_empty() {
return;
}

let tx_id = tx.id();
let descendants = self.inner.descendants(&tx_id);
for descendant in descendants.into_iter().filter(|descendant| *descendant != tx_id) {
if let Some(descendant) = self.inner.get_mut(&descendant) {
Arc::make_mut(descendant).mark_notes_authenticated(output_notes.iter().copied());
}
}
}

/// Number of transactions which have not been selected for inclusion in a batch.
pub fn unselected_count(&self) -> usize {
self.inner.node_count() - self.inner.selected_count()
Expand Down
57 changes: 56 additions & 1 deletion crates/block-producer/src/mempool/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use serial_test::serial;

use super::*;
use crate::mempool::graph::TransactionGraph;
use crate::test_utils::MockProvenTxBuilder;
use crate::test_utils::batch::TransactionBatchConstructor;
use crate::test_utils::{MockProvenTxBuilder, mock_account_id};

mod add_transaction;
mod add_user_batch;
Expand Down Expand Up @@ -178,6 +178,61 @@ fn empty_block_commitment() {
}
}

/// Regression test for a child transaction that consumes an unauthenticated note produced by a
/// parent transaction which has already been committed and later pruned from retained mempool
/// history.
///
/// The child remains in the transaction graph after the parent block is committed. Once retention
/// pruning removes the parent, the note is no longer represented by an inflight transaction, so the
/// child must stop reporting it as unauthenticated before it is selected into its own batch.
#[test]
fn pruned_committed_notes_are_authenticated_for_inflight_descendants() {
let (mut uut, _) = Mempool::for_tests();
uut.config.state_retention = NonZeroUsize::new(1).unwrap();

let parent = MockProvenTxBuilder::with_account(
mock_account_id(1),
Word::empty(),
Word::new([1u32.into(), 1u32.into(), 2u32.into(), 3u32.into()]),
)
.private_notes_created_range(3..4)
.build();
let parent = Arc::new(AuthenticatedTransaction::from_inner(parent));

let child = MockProvenTxBuilder::with_account(
mock_account_id(2),
Word::empty(),
Word::new([2u32.into(), 1u32.into(), 2u32.into(), 3u32.into()]),
)
.unauthenticated_notes_range(3..4)
.build();
let child = Arc::new(AuthenticatedTransaction::from_inner(child));

uut.add_transaction(parent.clone()).unwrap();
let parent_batch = uut.select_batch().unwrap();
assert_eq!(parent_batch.transactions(), std::slice::from_ref(&parent));

uut.add_transaction(child.clone()).unwrap();
uut.commit_batch(Arc::new(ProvenBatch::mocked_from_transactions([
parent.raw_proven_transaction()
])));

let block = uut.select_block();
let header = BlockHeader::mock(block.block_number, None, None, &[], Word::empty());
uut.commit_block(header);

let block = uut.select_block();
let header = BlockHeader::mock(block.block_number, None, None, &[], Word::empty());
uut.commit_block(header);

let child_batch = uut.select_batch().unwrap();

assert_eq!(child_batch.transactions().len(), 1);
assert_eq!(child_batch.transactions()[0].id(), child.id());
assert_eq!(child_batch.transactions()[0].unauthenticated_note_commitments().count(), 0);
assert_eq!(child_batch.unauthenticated_note_commitments().count(), 0);
}

#[test]
#[should_panic]
fn block_commitment_is_rejected_if_no_block_is_in_flight() {
Expand Down
Loading