cuprated/txpool/
relay_rules.rs

1use std::cmp::max;
2
3use monero_serai::transaction::Timelock;
4use thiserror::Error;
5
6use cuprate_consensus_context::BlockchainContext;
7use cuprate_consensus_rules::miner_tx::calculate_block_reward;
8use cuprate_helper::cast::usize_to_u64;
9use cuprate_types::TransactionVerificationData;
10
11/// The maximum size of the tx extra field.
12///
13/// <https://github.com/monero-project/monero/blob/3b01c490953fe92f3c6628fa31d280a4f0490d28/src/cryptonote_config.h#L217>
14const MAX_TX_EXTRA_SIZE: usize = 1060;
15
16/// <https://github.com/monero-project/monero/blob/3b01c490953fe92f3c6628fa31d280a4f0490d28/src/cryptonote_config.h#L75>
17const DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT: u128 = 3_000;
18
19/// <https://github.com/monero-project/monero/blob/3b01c490953fe92f3c6628fa31d280a4f0490d28/src/cryptonote_core/blockchain.h#L646>
20const FEE_MASK: u64 = 10_u64.pow(4);
21
22#[derive(Debug, Error)]
23pub enum RelayRuleError {
24    #[error("Tx has non-zero timelock.")]
25    NonZeroTimelock,
26    #[error("Tx extra field is too large.")]
27    ExtraFieldTooLarge,
28    #[error("Tx fee too low.")]
29    FeeBelowMinimum,
30}
31
32/// Checks the transaction passes the relay rules.
33///
34/// Relay rules are rules that govern the txs we accept to our tx-pool and propagate around the network.
35pub fn check_tx_relay_rules(
36    tx: &TransactionVerificationData,
37    context: &BlockchainContext,
38) -> Result<(), RelayRuleError> {
39    if tx.tx.prefix().additional_timelock != Timelock::None {
40        return Err(RelayRuleError::NonZeroTimelock);
41    }
42
43    if tx.tx.prefix().extra.len() > MAX_TX_EXTRA_SIZE {
44        return Err(RelayRuleError::ExtraFieldTooLarge);
45    }
46
47    check_fee(tx.tx_weight, tx.fee, context)
48}
49
50/// Checks the fee is enough for the tx weight and current blockchain state.
51fn check_fee(
52    tx_weight: usize,
53    fee: u64,
54    context: &BlockchainContext,
55) -> Result<(), RelayRuleError> {
56    let base_reward = calculate_block_reward(
57        1,
58        context.effective_median_weight,
59        context.already_generated_coins,
60        context.current_hf,
61    );
62
63    let fee_per_byte = dynamic_base_fee(base_reward, context.effective_median_weight);
64    let needed_fee = usize_to_u64(tx_weight) * fee_per_byte;
65
66    let needed_fee = needed_fee.div_ceil(FEE_MASK) * FEE_MASK;
67
68    if fee < (needed_fee - needed_fee / 50) {
69        tracing::debug!(fee, needed_fee, "Tx fee is below minimum.");
70        return Err(RelayRuleError::FeeBelowMinimum);
71    }
72
73    Ok(())
74}
75
76/// Calculates the base fee per byte for tx relay.
77fn dynamic_base_fee(base_reward: u64, effective_media_block_weight: usize) -> u64 {
78    let median_block_weight = effective_media_block_weight as u128;
79
80    let fee_per_byte_100 = u128::from(base_reward) * DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT
81        / median_block_weight
82        / median_block_weight;
83    let fee_per_byte = fee_per_byte_100 - fee_per_byte_100 / 20;
84
85    #[expect(clippy::cast_possible_truncation)]
86    max(fee_per_byte as u64, 1)
87}