Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
12 changes: 11 additions & 1 deletion cmd/ethrex/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::{

use clap::{ArgAction, Parser as ClapParser, Subcommand as ClapSubcommand};
use ethrex_blockchain::{
BlockchainOptions, BlockchainType, L2Config,
BlockchainOptions, BlockchainType, DEFAULT_MIN_TIP_WEI, L2Config,
error::{ChainError, InvalidBlockError},
};
use ethrex_common::types::{Block, DEFAULT_BUILDER_GAS_CEIL, Genesis, validate_block_body};
Expand Down Expand Up @@ -183,6 +183,15 @@ pub struct Options {
env = "ETHREX_MEMPOOL_MAX_SIZE"
)]
pub mempool_max_size: usize,
#[arg(
help = "Minimum effective priority fee (in wei) required for a transaction to be admitted into the mempool. For typed transactions this is `max_priority_fee_per_gas`; for legacy transactions it is `gas_price - base_fee`. Set to 0 to disable the floor.",
long = "mempool.min-tip",
default_value_t = DEFAULT_MIN_TIP_WEI,
value_name = "MIN_TIP_WEI",
help_heading = "Node options",
env = "ETHREX_MEMPOOL_MIN_TIP"
)]
Comment on lines +186 to +193
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

pub mempool_min_tip: u64,
#[arg(
long = "http.addr",
default_value = "0.0.0.0",
Expand Down Expand Up @@ -450,6 +459,7 @@ impl Default for Options {
dev: Default::default(),
force: false,
mempool_max_size: Default::default(),
mempool_min_tip: DEFAULT_MIN_TIP_WEI,
tx_broadcasting_time_interval: Default::default(),
target_peers: Default::default(),
lookup_interval: Default::default(),
Expand Down
1 change: 1 addition & 0 deletions cmd/ethrex/initializers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ pub async fn init_l1(
max_blobs_per_block: opts.max_blobs_per_block,
precompute_witnesses: opts.precompute_witnesses,
precompile_cache_enabled: !opts.no_precompile_cache,
min_tip_wei: opts.mempool_min_tip,
},
);

Expand Down
1 change: 1 addition & 0 deletions cmd/ethrex/l2/initializers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ pub async fn init_l2(
max_blobs_per_block: None, // L2 doesn't support blob transactions
precompute_witnesses: opts.node_opts.precompute_witnesses,
precompile_cache_enabled: true,
min_tip_wei: opts.node_opts.mempool_min_tip,
};

let blockchain = init_blockchain(store.clone(), blockchain_opts.clone());
Expand Down
40 changes: 39 additions & 1 deletion crates/blockchain/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ pub struct BlockchainOptions {
/// warmer thread and the executor. Set to false (via `--no-precompile-cache`) to
/// disable the cache for benchmarking purposes.
pub precompile_cache_enabled: bool,
/// Minimum effective priority fee (in wei) required for a transaction to be
/// admitted into the mempool. Transactions below this floor are rejected at
/// admission. Set to 0 to disable the floor.
pub min_tip_wei: u64,
}

impl Default for BlockchainOptions {
Expand All @@ -242,10 +246,15 @@ impl Default for BlockchainOptions {
max_blobs_per_block: None,
precompute_witnesses: false,
precompile_cache_enabled: true,
min_tip_wei: DEFAULT_MIN_TIP_WEI,
}
}
}

/// Default min-tip floor (wei). Matches geth's mempool `PriceLimit = 1 wei`.
/// Effectively just rejects zero-tip transactions at admission.
pub const DEFAULT_MIN_TIP_WEI: u64 = 1;

#[derive(Debug, Clone)]
pub struct BatchBlockProcessingFailure {
pub last_valid_hash: H256,
Expand Down Expand Up @@ -344,13 +353,20 @@ impl Blockchain {
}
}

/// Test-permissive `Blockchain` constructor. Mirrors `BlockchainOptions::default`
/// but disables admission-policy gates (e.g. the min-tip floor) so that
/// unrelated tests don't need to set every mempool option explicitly.
pub fn default_with_store(store: Store) -> Self {
let options = BlockchainOptions {
min_tip_wei: 0,
..BlockchainOptions::default()
};
Self {
storage: store,
mempool: Mempool::new(MAX_MEMPOOL_SIZE_DEFAULT),
is_synced: AtomicBool::new(false),
payloads: Arc::new(TokioMutex::new(Vec::new())),
options: BlockchainOptions::default(),
options,
merkle_pool: Self::build_merkle_pool(),
}
}
Expand Down Expand Up @@ -2471,6 +2487,28 @@ impl Blockchain {
return Err(MempoolError::TxTipAboveFeeCapError);
}

// Admission-time minimum tip floor. For typed (1559-family) txs, the
// effective tip is `max_priority_fee_per_gas`. For legacy / EIP-2930
// it's `gas_price.saturating_sub(base_fee)`. A floor of 0 disables
// the check. Saturating arithmetic guards against pre-London headers
// where `base_fee_per_gas` is `None`.
if self.options.min_tip_wei > 0 {
let effective_tip = match tx.max_priority_fee() {
Some(tip) => tip,
None => {
let base_fee = header.base_fee_per_gas.unwrap_or(0);
let gas_price_u64 = u64::try_from(tx.gas_price()).unwrap_or(u64::MAX);
gas_price_u64.saturating_sub(base_fee)
}
};
if effective_tip < self.options.min_tip_wei {
return Err(MempoolError::TipBelowMinimum {
actual: effective_tip,
limit: self.options.min_tip_wei,
});
}
}

// Check that the gas limit covers the gas needs for transaction metadata.
if tx.gas_limit() < mempool::transaction_intrinsic_gas(tx, &header, &config)? {
return Err(MempoolError::TxIntrinsicGasCostAboveLimitError);
Expand Down
2 changes: 2 additions & 0 deletions crates/blockchain/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ pub enum MempoolError {
TxMaxInitCodeSizeError,
#[error("Transaction max data size exceeded")]
TxMaxDataSizeError,
#[error("Effective tip {actual} wei below the configured minimum of {limit} wei")]
TipBelowMinimum { actual: u64, limit: u64 },
#[error("Transaction gas limit exceeded")]
TxGasLimitExceededError,
#[error(
Expand Down
7 changes: 3 additions & 4 deletions crates/common/types/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ pub const VERSIONED_HASH_VERSION_KZG: u8 = 0x01; // Defined in [EIP-4844](https:
/// Minimum tip, obtained from geth's default miner config (https://github.com/ethereum/go-ethereum/blob/f750117ad19d623622cc4a46ea361a716ba7407e/miner/miner.go#L56)
///
/// Scope: this constant is consumed only by the RPC gas-price estimators
/// (`eth_gasPrice`, `eth_maxPriorityFeePerGas`). It is NOT a mempool
/// admission gate — zero-tip transactions are currently admitted.
///
/// TODO: This should be configurable along with the tip filter on https://github.com/lambdaclass/ethrex/issues/680
/// (`eth_gasPrice`, `eth_maxPriorityFeePerGas`). The mempool admission
/// floor is a separate, lower default (see
/// `ethrex_blockchain::DEFAULT_MIN_TIP_WEI`).
pub const MIN_GAS_TIP: u64 = 1000000;

// Blob size related
Expand Down
6 changes: 6 additions & 0 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ Node options:
[env: ETHREX_MEMPOOL_MAX_SIZE=]
[default: 10000]

--mempool.min-tip <MIN_TIP_WEI>
Minimum effective priority fee (in wei) required for a transaction to be admitted into the mempool. For typed transactions this is `max_priority_fee_per_gas`; for legacy transactions it is `gas_price - base_fee`. Set to 0 to disable the floor.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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


[env: ETHREX_MEMPOOL_MIN_TIP=]
[default: 1]

--precompute-witnesses
Once synced, computes execution witnesses upon receiving newPayload messages and stores them in local storage

Expand Down
145 changes: 145 additions & 0 deletions test/tests/blockchain/mempool_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use ethrex_blockchain::Blockchain;
use ethrex_blockchain::BlockchainOptions;
use ethrex_blockchain::constants::MAX_INITCODE_SIZE;
use ethrex_blockchain::constants::{
TX_ACCESS_LIST_ADDRESS_GAS, TX_ACCESS_LIST_STORAGE_KEY_GAS, TX_CREATE_GAS_COST,
Expand Down Expand Up @@ -467,3 +468,147 @@ fn blobs_bundle_insert_and_remove() {
vec![None]
);
}

// --- min-tip floor admission tests ----------------------------------------

/// Builds a blockchain configured with `min_tip_wei` as the admission floor
/// (everything else permissive for tests).
fn blockchain_with_min_tip(store: Store, min_tip_wei: u64) -> Blockchain {
let mut bc = Blockchain::default_with_store(store);
let mut opts = bc.options.clone();
opts.min_tip_wei = min_tip_wei;
bc.options = opts;
bc
}

#[tokio::test]
async fn zero_tip_eip1559_rejected_under_default_floor() {
let (config, header) = build_basic_config_and_header(false, false);
let store = setup_storage(config, header).await.expect("Storage setup");
let blockchain = blockchain_with_min_tip(store, 1_000_000);

let tx = EIP1559Transaction {
max_priority_fee_per_gas: 0,
max_fee_per_gas: 1_000_000,
gas_limit: 50_000_000,
to: TxKind::Call(Address::from_low_u64_be(1)),
..Default::default()
};
let tx = Transaction::EIP1559Transaction(tx);

let res = blockchain
.validate_transaction(&tx, Address::random())
.await;
assert!(matches!(
res,
Err(MempoolError::TipBelowMinimum {
actual: 0,
limit: 1_000_000,
}),
));
}

#[tokio::test]
Comment on lines +585 to +592
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Negative assertions mask unrelated validation failures

at_floor_eip1559_passes_tip_check and floor_of_zero_admits_zero_tip both use assert!(!matches!(res, Err(MempoolError::TipBelowMinimum { .. }))). This passes when validate_transaction returns Ok(_) OR when it returns any other Err variant. If a future refactor reorders checks and the tip guard is accidentally skipped while another error fires, these tests would still pass, providing false confidence. The real fix is to assert Ok(_) or to explicitly document that a different error is acceptable here.

Prompt To Fix With AI
This is a comment left during a code review.
Path: test/tests/blockchain/mempool_tests.rs
Line: 504-511

Comment:
**Negative assertions mask unrelated validation failures**

`at_floor_eip1559_passes_tip_check` and `floor_of_zero_admits_zero_tip` both use `assert!(!matches!(res, Err(MempoolError::TipBelowMinimum { .. })))`. This passes when `validate_transaction` returns `Ok(_)` OR when it returns any other `Err` variant. If a future refactor reorders checks and the tip guard is accidentally skipped while another error fires, these tests would still pass, providing false confidence. The real fix is to assert `Ok(_)` or to explicitly document that a different error is acceptable here.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

async fn at_floor_eip1559_passes_tip_check() {
// Tip at the floor passes the min-tip check. The tx may still fail later
// (intrinsic gas, balance, etc.) — we only assert the tip check itself
// does not fire.
let (config, header) = build_basic_config_and_header(false, false);
let store = setup_storage(config, header).await.expect("Storage setup");
let blockchain = blockchain_with_min_tip(store, 1_000_000);

let tx = EIP1559Transaction {
max_priority_fee_per_gas: 1_000_000,
max_fee_per_gas: 1_000_000,
gas_limit: 50_000_000,
to: TxKind::Call(Address::from_low_u64_be(1)),
..Default::default()
};
let tx = Transaction::EIP1559Transaction(tx);

let res = blockchain
.validate_transaction(&tx, Address::random())
.await;
assert!(!matches!(res, Err(MempoolError::TipBelowMinimum { .. })));
}

#[tokio::test]
async fn floor_of_zero_admits_zero_tip() {
// Operators can disable the floor with --mempool.min-tip 0.
let (config, header) = build_basic_config_and_header(false, false);
let store = setup_storage(config, header).await.expect("Storage setup");
let blockchain = blockchain_with_min_tip(store, 0);

let tx = EIP1559Transaction {
max_priority_fee_per_gas: 0,
max_fee_per_gas: 0,
gas_limit: 50_000_000,
to: TxKind::Call(Address::from_low_u64_be(1)),
..Default::default()
};
let tx = Transaction::EIP1559Transaction(tx);

let res = blockchain
.validate_transaction(&tx, Address::random())
.await;
assert!(!matches!(res, Err(MempoolError::TipBelowMinimum { .. })));
}

#[tokio::test]
async fn legacy_effective_tip_below_floor_rejected() {
// Legacy tx: effective_tip = gas_price.saturating_sub(base_fee).
// Test header has no base_fee (None → 0), so effective_tip = gas_price.
let (config, header) = build_basic_config_and_header(false, false);
let store = setup_storage(config, header).await.expect("Storage setup");
let blockchain = blockchain_with_min_tip(store, 1_000_000);

let tx = ethrex_common::types::LegacyTransaction {
gas_price: U256::from(999_999u64), // 1 wei below floor
gas: 50_000_000,
to: TxKind::Call(Address::from_low_u64_be(1)),
..Default::default()
};
let tx = Transaction::LegacyTransaction(tx);

let res = blockchain
.validate_transaction(&tx, Address::random())
.await;
assert!(matches!(
res,
Err(MempoolError::TipBelowMinimum {
actual: 999_999,
limit: 1_000_000,
}),
));
}

#[tokio::test]
async fn options_field_is_used_in_validate_transaction() {
// Smoke test that BlockchainOptions::min_tip_wei is consulted (not
// accidentally ignored if the option-plumbing breaks).
let (config, header) = build_basic_config_and_header(false, false);
let store = setup_storage(config, header).await.expect("Storage setup");

let mut bc = Blockchain::default_with_store(store);
let mut opts = BlockchainOptions::default();
opts.min_tip_wei = 5_000_000_000; // 5 gwei
bc.options = opts;

let tx = EIP1559Transaction {
max_priority_fee_per_gas: 1_000_000_000, // 1 gwei (below 5 gwei)
max_fee_per_gas: 5_000_000_000,
gas_limit: 50_000_000,
to: TxKind::Call(Address::from_low_u64_be(1)),
..Default::default()
};
let tx = Transaction::EIP1559Transaction(tx);

let res = bc.validate_transaction(&tx, Address::random()).await;
assert!(matches!(
res,
Err(MempoolError::TipBelowMinimum {
actual: 1_000_000_000,
limit: 5_000_000_000,
}),
));
}
Loading