Skip to content
Merged
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 @@ -17,6 +17,7 @@

### Changes

- Refactored NTX Builder actor state into `AccountDeltaTracker` and `NotePool` for clarity, and added tracing instrumentation to event broadcasting ([#1611](https://github.com/0xMiden/miden-node/pull/1611)).
- Refactored NTX Builder startup and introduced `NtxBuilderConfig` with configurable parameters ([#1610](https://github.com/0xMiden/miden-node/pull/1610)).

## v0.13.2 (2026-01-27)
Expand Down
73 changes: 44 additions & 29 deletions crates/ntx-builder/src/actor/account_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use miden_protocol::transaction::{PartialBlockchain, TransactionId};
use tracing::instrument;

use super::ActorShutdownReason;
use super::note_state::{NetworkAccountEffect, NetworkAccountNoteState};
use super::note_state::{AccountDeltaTracker, NetworkAccountEffect, NotePool};
use crate::COMPONENT;
use crate::actor::inflight_note::InflightNetworkNote;
use crate::builder::ChainState;
Expand Down Expand Up @@ -49,25 +49,26 @@ pub struct TransactionCandidate {
/// The current state of a network account.
#[derive(Clone)]
pub struct NetworkAccountState {
/// The network account ID corresponding to the network account this state represents.
/// The network account ID this state represents.
account_id: NetworkAccountId,

/// Component of this state which Contains the committed and inflight account updates as well
/// as available and nullified notes.
account: NetworkAccountNoteState,
/// Tracks committed and inflight account state updates.
account: AccountDeltaTracker,

/// Manages available and nullified notes.
notes: NotePool,

/// Uncommitted transactions which have some impact on the network state.
///
/// This is tracked so we can commit or revert such transaction effects. Transactions _without_
/// an impact are ignored.
/// This is tracked so we can commit or revert transaction effects. Transactions _without_ an
/// impact are ignored.
inflight_txs: BTreeMap<TransactionId, TransactionImpact>,

/// Nullifiers of all network notes targeted at this account.
///
/// Used to filter mempool events: when a `TransactionAdded` event reports consumed nullifiers,
/// only those present in this set are processed (moved from `available_notes` to
/// `nullified_notes`). Nullifiers are added when notes are loaded or created, and removed
/// when the consuming transaction is committed.
/// only those present in this set are processed. Nullifiers are added when notes are loaded
/// or created, and removed when the consuming transaction is committed.
known_nullifiers: HashSet<Nullifier>,
}

Expand All @@ -92,10 +93,15 @@ impl NetworkAccountState {
let known_nullifiers: HashSet<Nullifier> =
notes.iter().map(SingleTargetNetworkNote::nullifier).collect();

let account = NetworkAccountNoteState::new(account, notes);
let account_tracker = AccountDeltaTracker::new(account);
let mut note_pool = NotePool::default();
for note in notes {
note_pool.add_note(note);
}

let state = Self {
account,
account: account_tracker,
notes: note_pool,
account_id,
inflight_txs: BTreeMap::default(),
known_nullifiers,
Expand All @@ -121,17 +127,17 @@ impl NetworkAccountState {
chain_state: ChainState,
) -> Option<TransactionCandidate> {
// Remove notes that have failed too many times.
self.account.drop_failing_notes(max_note_attempts);
self.notes.drop_failing_notes(max_note_attempts);

// Skip empty accounts, and prune them.
// This is how we keep the number of accounts bounded.
if self.account.is_empty() {
if self.is_empty() {
return None;
}

// Select notes from the account that can be consumed or are ready for a retry.
let notes = self
.account
.notes
.available_notes(&chain_state.chain_tip_header.block_num())
.take(limit.get())
.cloned()
Expand All @@ -158,7 +164,7 @@ impl NetworkAccountState {
#[instrument(target = COMPONENT, name = "ntx.state.notes_failed", skip_all)]
pub fn notes_failed(&mut self, notes: &[Note], block_num: BlockNumber) {
let nullifiers = notes.iter().map(Note::nullifier).collect::<Vec<_>>();
self.account.fail_notes(nullifiers.as_slice(), block_num);
self.notes.fail_notes(nullifiers.as_slice(), block_num);
}

/// Updates state with the mempool event.
Expand Down Expand Up @@ -201,6 +207,11 @@ impl NetworkAccountState {
None
}

/// Returns `true` if there is no inflight state being tracked.
fn is_empty(&self) -> bool {
self.account.has_no_inflight() && self.notes.is_empty()
}

/// Handles a [`MempoolEvent::TransactionAdded`] event.
fn add_transaction(
&mut self,
Expand Down Expand Up @@ -238,16 +249,15 @@ impl NetworkAccountState {
);
tx_impact.notes.insert(note.nullifier());
self.known_nullifiers.insert(note.nullifier());
self.account.add_note(note.clone());
self.notes.add_note(note.clone());
}
for nullifier in nullifiers {
// Ignore nullifiers that aren't network note nullifiers.
if !self.known_nullifiers.contains(nullifier) {
continue;
}
tx_impact.nullifiers.insert(*nullifier);
// We don't use the entry wrapper here because the account must already exist.
let _ = self.account.add_nullifier(*nullifier);
let _ = self.notes.nullify(*nullifier);
}

if !tx_impact.is_empty() {
Expand All @@ -272,7 +282,7 @@ impl NetworkAccountState {
if self.known_nullifiers.remove(&nullifier) {
// Its possible for the account to no longer exist if the transaction creating it
// was reverted.
self.account.commit_nullifier(nullifier);
self.notes.commit_nullifier(nullifier);
}
}
}
Expand All @@ -296,15 +306,15 @@ impl NetworkAccountState {
// Revert notes.
for note_nullifier in impact.notes {
if self.known_nullifiers.contains(&note_nullifier) {
self.account.revert_note(note_nullifier);
self.notes.remove_note(note_nullifier);
self.known_nullifiers.remove(&note_nullifier);
}
}

// Revert nullifiers.
for nullifier in impact.nullifiers {
if self.known_nullifiers.contains(&nullifier) {
self.account.revert_nullifier(nullifier);
self.notes.revert_nullifier(nullifier);
self.known_nullifiers.remove(&nullifier);
}
}
Expand Down Expand Up @@ -475,10 +485,15 @@ mod tests {
let known_nullifiers: HashSet<Nullifier> =
notes.iter().map(SingleTargetNetworkNote::nullifier).collect();

let account = NetworkAccountNoteState::new(account, notes);
let account_tracker = AccountDeltaTracker::new(account);
let mut note_pool = NotePool::default();
for note in notes {
note_pool.add_note(note);
}

Self {
account,
account: account_tracker,
notes: note_pool,
account_id,
inflight_txs: BTreeMap::default(),
known_nullifiers,
Expand Down Expand Up @@ -538,7 +553,7 @@ mod tests {
let mut state =
NetworkAccountState::new_for_testing(account, network_account_id, vec![note1, note2]);

let available_count = state.account.available_notes(&BlockNumber::from(0)).count();
let available_count = state.notes.available_notes(&BlockNumber::from(0)).count();
assert_eq!(available_count, 2, "both notes should be available initially");

let tx_id = mock_tx_id(1);
Expand All @@ -553,7 +568,7 @@ mod tests {
assert!(shutdown.is_none(), "mempool_update should not trigger shutdown");

let available_nullifiers: Vec<_> = state
.account
.notes
.available_notes(&BlockNumber::from(0))
.map(|n| n.to_inner().nullifier())
.collect();
Expand Down Expand Up @@ -634,7 +649,7 @@ mod tests {
state.mempool_update(&event);

// Verify note is not available
let available_count = state.account.available_notes(&BlockNumber::from(0)).count();
let available_count = state.notes.available_notes(&BlockNumber::from(0)).count();
assert_eq!(available_count, 0, "note should not be available after being consumed");

// Revert the transaction
Expand All @@ -644,7 +659,7 @@ mod tests {

// Verify note is available again
let available_nullifiers: Vec<_> = state
.account
.notes
.available_notes(&BlockNumber::from(0))
.map(|n| n.to_inner().nullifier())
.collect();
Expand Down Expand Up @@ -687,7 +702,7 @@ mod tests {

// Verify the note is available
let available_nullifiers: Vec<_> = state
.account
.notes
.available_notes(&BlockNumber::from(0))
.map(|n| n.to_inner().nullifier())
.collect();
Expand Down
Loading