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 binaries/cuprated/src/txpool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use cuprate_txpool::service::{TxpoolReadHandle, TxpoolWriteHandle};

mod dandelion;
mod incoming_tx;
mod relay_rules;
mod txs_being_handled;

pub use incoming_tx::{IncomingTxError, IncomingTxHandler, IncomingTxs};
9 changes: 9 additions & 0 deletions binaries/cuprated/src/txpool/incoming_tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ use crate::{
signals::REORG_LOCK,
txpool::{
dandelion,
relay_rules::check_tx_relay_rules,
txs_being_handled::{TxsBeingHandled, TxsBeingHandledLocally},
},
};
Expand Down Expand Up @@ -179,6 +180,14 @@ async fn handle_incoming_txs(
.map_err(IncomingTxError::Consensus)?;

for tx in txs {
// TODO: this could be a DoS, if someone spams us with txs that violate these rules?
// Maybe we should remember these invalid txs for some time to prevent them getting repeatedly sent.
if let Err(e) = check_tx_relay_rules(&tx, context) {
tracing::debug!(err = %e, tx = hex::encode(tx.tx_hash), "Tx failed relay check, skipping.");

continue;
}

handle_valid_tx(
tx,
state.clone(),
Expand Down
87 changes: 87 additions & 0 deletions binaries/cuprated/src/txpool/relay_rules.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::cmp::max;

use monero_serai::transaction::Timelock;
use thiserror::Error;

use cuprate_consensus_context::BlockchainContext;
use cuprate_consensus_rules::miner_tx::calculate_block_reward;
use cuprate_helper::cast::usize_to_u64;
use cuprate_types::TransactionVerificationData;

/// The maximum size of the tx extra field.
///
/// <https://github.com/monero-project/monero/blob/3b01c490953fe92f3c6628fa31d280a4f0490d28/src/cryptonote_config.h#L217>
const MAX_TX_EXTRA_SIZE: usize = 1060;

/// <https://github.com/monero-project/monero/blob/3b01c490953fe92f3c6628fa31d280a4f0490d28/src/cryptonote_config.h#L75>
const DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT: u128 = 3_000;
Comment thread
Boog900 marked this conversation as resolved.

/// <https://github.com/monero-project/monero/blob/3b01c490953fe92f3c6628fa31d280a4f0490d28/src/cryptonote_core/blockchain.h#L646>
const FEE_MASK: u64 = 10_u64.pow(4);
Comment thread
Boog900 marked this conversation as resolved.

#[derive(Debug, Error)]
pub enum RelayRuleError {
#[error("Tx has non-zero timelock.")]
NonZeroTimelock,
#[error("Tx extra field is too large.")]
ExtraFieldTooLarge,
#[error("Tx fee too low.")]
FeeBelowMinimum,
}

/// Checks the transaction passes the relay rules.
///
/// Relay rules are rules that govern the txs we accept to our tx-pool and propagate around the network.
pub fn check_tx_relay_rules(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some questions:

Copy link
Copy Markdown
Member Author

@Boog900 Boog900 Apr 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seem to be other relay rules not included here, e.g.: https://github.com/monero-project/monero/blob/3b01c490953fe92f3c6628fa31d280a4f0490d28/src/cryptonote_core/tx_pool.cpp#L150-L151, should the diff between monerod and cuprated relay rule behavior be documented somewhere?

That one isn't really a relay rule IMO, it just prevents a previously timed out tx from getting added to the pool again.

All relay rules should be included here.

Should check_fee/dynamic_base_fee have some tests to make sure it matches monerod over time?

Probably, I don't know how we would do that without generating txs though so that will have to come later

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

monerod's tx-pool also deals with some consensus checks, only the checks that have m_no_drop_offense = true are relay checks

tx: &TransactionVerificationData,
context: &BlockchainContext,
) -> Result<(), RelayRuleError> {
if tx.tx.prefix().additional_timelock != Timelock::None {
return Err(RelayRuleError::NonZeroTimelock);
}

if tx.tx.prefix().extra.len() > MAX_TX_EXTRA_SIZE {
return Err(RelayRuleError::ExtraFieldTooLarge);
}

check_fee(tx.tx_weight, tx.fee, context)
}

/// Checks the fee is enough for the tx weight and current blockchain state.
fn check_fee(
tx_weight: usize,
fee: u64,
context: &BlockchainContext,
) -> Result<(), RelayRuleError> {
let base_reward = calculate_block_reward(
1,
context.effective_median_weight,
context.already_generated_coins,
context.current_hf,
);

let fee_per_byte = dynamic_base_fee(base_reward, context.effective_median_weight);
let needed_fee = usize_to_u64(tx_weight) * fee_per_byte;

let needed_fee = needed_fee.div_ceil(FEE_MASK) * FEE_MASK;

if fee < (needed_fee - needed_fee / 50) {
tracing::debug!(fee, needed_fee, "Tx fee is below minimum.");
return Err(RelayRuleError::FeeBelowMinimum);
}

Ok(())
}

/// Calculates the base fee per byte for tx relay.
fn dynamic_base_fee(base_reward: u64, effective_media_block_weight: usize) -> u64 {
let median_block_weight = effective_media_block_weight as u128;

let fee_per_byte_100 = u128::from(base_reward) * DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT
/ median_block_weight
/ median_block_weight;
let fee_per_byte = fee_per_byte_100 - fee_per_byte_100 / 20;

#[expect(clippy::cast_possible_truncation)]
max(fee_per_byte as u64, 1)
}