Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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