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
6 changes: 6 additions & 0 deletions crates/block-producer/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ pub enum MempoolSubmissionError {
#[error("transaction {transaction_id} does not contain a non-zero TX_FEE output note")]
MissingFee { transaction_id: TransactionId },

#[error("transaction {transaction_id} consumes in-flight TX_FEE notes: {note_ids:?}")]
ConsumesInflightFeeNotes {
transaction_id: TransactionId,
note_ids: Vec<Word>,
},

#[error("mempool lock is poisoned")]
#[grpc(internal)]
MempoolPoisoned(#[source] MempoolPoisonError),
Expand Down
6 changes: 6 additions & 0 deletions crates/block-producer/src/mempool/graph/dag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,12 @@ where
pub(super) fn get_mut(&mut self, node: &N::Id) -> Option<&mut N> {
self.nodes.get_mut(node)
}

/// Returns the node that created the specified note.
pub(super) fn note_creator(&self, note: &miden_protocol::Word) -> Option<&N> {
Comment thread
Mirko-von-Leipzig marked this conversation as resolved.
let creator = self.state.note_creator(note)?;
self.nodes.get(&creator)
}
}

// GRAPH DAG TESTS
Expand Down
5 changes: 5 additions & 0 deletions crates/block-producer/src/mempool/graph/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ where
pub fn output_note_count(&self) -> usize {
self.notes_created.len()
}

/// Returns the node that created the specified note.
pub(super) fn note_creator(&self, note: &Word) -> Option<K> {
self.notes_created.get(note).copied()
}
}

/// Tracks the per-account state transitions that are in-flight within the mempool graph.
Expand Down
15 changes: 14 additions & 1 deletion crates/block-producer/src/mempool/graph/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use miden_protocol::account::AccountId;
use miden_protocol::batch::BatchId;
use miden_protocol::block::BlockNumber;
use miden_protocol::note::Nullifier;
use miden_protocol::transaction::TransactionId;
use miden_protocol::transaction::{OutputNote, TransactionId};

use crate::domain::batch::{BatchParameters, SelectedBatch};
use crate::errors::StateConflict;
Expand Down Expand Up @@ -134,6 +134,19 @@ impl TransactionGraph {
self.inner.append(tx)
}

/// Returns the transaction and output note that created the specified note ID.
pub fn output_note(&self, note_id: Word) -> Option<(TransactionId, &OutputNote)> {
let creator = self.inner.note_creator(&note_id)?;
let output_note = creator
.raw_proven_transaction()
.output_notes()
.iter()
.find(|note| note.id().as_word() == note_id)
.expect("the note creator must contain the indexed output note");

Some((creator.id(), output_note))
}

/// Appends the transactions into the graph as an atomic unit.
///
/// These transactions can only be selected as a batch, and are reverted and pruned together.
Expand Down
44 changes: 43 additions & 1 deletion crates/block-producer/src/mempool/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ use miden_node_proto::domain::sequencer::AuthenticatedTransaction;
use miden_node_tracing::{ErrorReport, debug, miden_instrument, miden_span_record};
use miden_protocol::batch::{BatchId, ProvenBatch};
use miden_protocol::block::{BlockHeader, BlockNumber};
use miden_protocol::transaction::TransactionHeader;
use miden_protocol::transaction::{OutputNote, TransactionHeader, TransactionId};
use miden_standards::note::TxFeeNote;
use thiserror::Error;

use crate::block_builder::SelectedBlock;
Expand Down Expand Up @@ -251,6 +252,7 @@ impl Mempool {

self.authentication_staleness_check(tx.authentication_height())?;
self.expiration_check(tx.expires_at())?;
self.fee_note_consumption_check(&tx)?;

// Insert the transaction node.
self.transactions
Expand Down Expand Up @@ -722,6 +724,46 @@ impl Mempool {

Ok(())
}

/// Rejects transactions that consume an uncommitted `TX_FEE` note.
fn fee_note_consumption_check(
&self,
tx: &AuthenticatedTransaction,
) -> Result<(), MempoolSubmissionError> {
let fee_script_root = TxFeeNote::script_root();
let is_fee_note = |note: &OutputNote| {
note.recipient()
.is_some_and(|recipient| recipient.script().root() == fee_script_root)
};

let note_ids = tx
.unauthenticated_note_ids()
.filter(|note_id| {
let Some((creator, note)) = self.transactions.output_note(*note_id) else {
return false;
};

is_fee_note(note) && !self.transaction_is_committed(creator)
})
.collect::<Vec<_>>();
Comment thread
PhilippGackstatter marked this conversation as resolved.

if note_ids.is_empty() {
Ok(())
} else {
Err(MempoolSubmissionError::ConsumesInflightFeeNotes {
transaction_id: tx.id(),
note_ids,
})
}
}

fn transaction_is_committed(&self, transaction_id: TransactionId) -> bool {
self.committed_blocks
.iter()
.flat_map(|block| block.batches.iter())
.flat_map(|batch| batch.transactions().as_slice())
.any(|transaction| transaction.id() == transaction_id)
}
}

fn emit_transaction_added(tx: &AuthenticatedTransaction) {
Expand Down
58 changes: 58 additions & 0 deletions crates/block-producer/src/mempool/tests/add_transaction.rs
Comment thread
Mirko-von-Leipzig marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
use std::sync::Arc;

use assert_matches::assert_matches;
use miden_node_proto::domain::sequencer::AuthenticatedTransaction;
use miden_protocol::Word;
use miden_protocol::batch::ProvenBatch;
use miden_protocol::block::BlockHeader;
use miden_protocol::transaction::{OutputNote, PublicOutputNote};

use crate::errors::{MempoolSubmissionError, StateConflict};
use crate::mempool::Mempool;
use crate::test_utils::batch::TransactionBatchConstructor;
use crate::test_utils::note::mock_fee_note;
use crate::test_utils::{MockAuthenticatedTxBuilder, MockProvenTxBuilder, mock_account_id};

#[test]
Expand Down Expand Up @@ -313,6 +318,59 @@ fn unknown_unauthenticated_notes_are_rejected() {
);
}

#[test]
fn inflight_fee_note_consumption_is_rejected() {
let (mut uut, _) = Mempool::for_tests();
let (producer, consumer, fee_note_id) = fee_note_dependency();

uut.add_transaction(producer).unwrap();
let reference = uut.clone();
let result = uut.add_transaction(consumer.clone());

assert_matches!(
result,
Err(MempoolSubmissionError::ConsumesInflightFeeNotes {
transaction_id,
note_ids,
}) if transaction_id == consumer.id() && note_ids == vec![fee_note_id]
);
assert_eq!(uut, reference);
}

#[test]
fn committed_fee_note_consumption_is_accepted() {
let (mut uut, _) = Mempool::for_tests();
let (producer, consumer, _) = fee_note_dependency();

uut.add_transaction(producer.clone()).unwrap();
uut.select_any_batch().unwrap();
uut.commit_batch(Arc::new(ProvenBatch::mocked_from_transactions([
producer.raw_proven_transaction()
])));
let block = uut.select_block();
let header = BlockHeader::mock(block.block_number, None, None, &[]);
uut.commit_block(&header);

uut.add_transaction(consumer).unwrap();
}

fn fee_note_dependency() -> (Arc<AuthenticatedTransaction>, Arc<AuthenticatedTransaction>, Word) {
let fee_note = mock_fee_note(100);
let fee_note_id = fee_note.id().as_word();
let producer = MockProvenTxBuilder::with_account_index(100)
.output_notes(vec![OutputNote::Public(PublicOutputNote::new(fee_note.clone()).unwrap())])
.build();
let consumer = MockProvenTxBuilder::with_account_index(101)
.unauthenticated_notes(vec![fee_note])
.build();

(
Arc::new(MockAuthenticatedTxBuilder::new(producer).build()),
Arc::new(MockAuthenticatedTxBuilder::new(consumer).build()),
fee_note_id,
)
}

mod account_state {
use super::*;

Expand Down
15 changes: 15 additions & 0 deletions crates/block-producer/src/test_utils/note.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use miden_protocol::Word;
use miden_protocol::asset::FungibleAsset;
use miden_protocol::note::Note;
use miden_protocol::transaction::{OutputNote, PublicOutputNote};
use miden_standards::note::TxFeeNote;
use miden_standards::testing::note::NoteBuilder;
use rand_chacha::ChaCha20Rng;
use rand_chacha::rand_core::SeedableRng;
Expand All @@ -14,3 +17,15 @@ pub fn mock_note(num: u8) -> Note {
pub fn mock_output_note(num: u8) -> OutputNote {
OutputNote::Public(PublicOutputNote::new(mock_note(num)).unwrap())
}

pub fn mock_fee_note(num: u8) -> Note {
let asset = FungibleAsset::new(FungibleAsset::mock_issuer(), u64::from(num) + 1).unwrap();

TxFeeNote::builder()
.sender(mock_account_id(num))
.serial_number(Word::from([u32::from(num), 0, 0, 0]))
.asset(asset)
.build()
.unwrap()
.into()
}
Loading