mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-08 03:01:12 -04:00
feat: adding cli --rpc.txfeecap flag (#15654)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
@@ -18,7 +18,7 @@ pub use load_secret_key::get_secret_key;
|
||||
pub mod parsers;
|
||||
pub use parsers::{
|
||||
hash_or_num_value_parser, parse_duration_from_secs, parse_duration_from_secs_or_ms,
|
||||
parse_socket_address,
|
||||
parse_ether_value, parse_socket_address,
|
||||
};
|
||||
|
||||
#[cfg(all(unix, any(target_env = "gnu", target_os = "macos")))]
|
||||
|
||||
@@ -89,6 +89,23 @@ pub fn read_json_from_file<T: serde::de::DeserializeOwned>(path: &str) -> Result
|
||||
reth_fs_util::read_json_file(Path::new(path))
|
||||
}
|
||||
|
||||
/// Parses an ether value from a string.
|
||||
///
|
||||
/// The amount in eth like "1.05" will be interpreted in wei (1.05 * 1e18).
|
||||
/// Supports both decimal and integer inputs.
|
||||
///
|
||||
/// # Examples
|
||||
/// - "1.05" -> 1.05 ETH = 1.05 * 10^18 wei
|
||||
/// - "2" -> 2 ETH = 2 * 10^18 wei
|
||||
pub fn parse_ether_value(value: &str) -> eyre::Result<u128> {
|
||||
let eth = value.parse::<f64>()?;
|
||||
if eth.is_sign_negative() {
|
||||
return Err(eyre::eyre!("Ether value cannot be negative"))
|
||||
}
|
||||
let wei = eth * 1e18;
|
||||
Ok(wei as u128)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -131,4 +148,25 @@ mod tests {
|
||||
|
||||
assert!(parse_duration_from_secs_or_ms("5ns").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ether_values() {
|
||||
// Test basic decimal value
|
||||
let wei = parse_ether_value("1.05").unwrap();
|
||||
assert_eq!(wei, 1_050_000_000_000_000_000u128);
|
||||
|
||||
// Test integer value
|
||||
let wei = parse_ether_value("2").unwrap();
|
||||
assert_eq!(wei, 2_000_000_000_000_000_000u128);
|
||||
|
||||
// Test zero
|
||||
let wei = parse_ether_value("0").unwrap();
|
||||
assert_eq!(wei, 0);
|
||||
|
||||
// Test negative value fails
|
||||
assert!(parse_ether_value("-1").is_err());
|
||||
|
||||
// Test invalid input fails
|
||||
assert!(parse_ether_value("abc").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +363,7 @@ where
|
||||
.with_head_timestamp(ctx.head().timestamp)
|
||||
.kzg_settings(ctx.kzg_settings()?)
|
||||
.with_local_transactions_config(pool_config.local_transactions_config.clone())
|
||||
.set_tx_fee_cap(ctx.config().rpc.rpc_tx_fee_cap)
|
||||
.with_additional_tasks(ctx.config().txpool.additional_validation_tasks)
|
||||
.build_with_tasks(ctx.task_executor().clone(), blob_store.clone());
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use clap::{
|
||||
Arg, Args, Command,
|
||||
};
|
||||
use rand::Rng;
|
||||
use reth_cli_util::parse_ether_value;
|
||||
use reth_rpc_server_types::{constants, RethRpcModule, RpcModuleSelection};
|
||||
|
||||
use crate::args::{
|
||||
@@ -169,6 +170,16 @@ pub struct RpcServerArgs {
|
||||
)]
|
||||
pub rpc_gas_cap: u64,
|
||||
|
||||
/// Maximum eth transaction fee that can be sent via the RPC APIs (0 = no cap)
|
||||
#[arg(
|
||||
long = "rpc.txfeecap",
|
||||
alias = "rpc-txfeecap",
|
||||
value_name = "TX_FEE_CAP",
|
||||
value_parser = parse_ether_value,
|
||||
default_value = "1.0"
|
||||
)]
|
||||
pub rpc_tx_fee_cap: u128,
|
||||
|
||||
/// Maximum number of blocks for `eth_simulateV1` call.
|
||||
#[arg(
|
||||
long = "rpc.max-simulate-blocks",
|
||||
@@ -329,6 +340,7 @@ impl Default for RpcServerArgs {
|
||||
rpc_max_blocks_per_filter: constants::DEFAULT_MAX_BLOCKS_PER_FILTER.into(),
|
||||
rpc_max_logs_per_response: (constants::DEFAULT_MAX_LOGS_PER_RESPONSE as u64).into(),
|
||||
rpc_gas_cap: constants::gas_oracle::RPC_DEFAULT_GAS_CAP,
|
||||
rpc_tx_fee_cap: constants::DEFAULT_TX_FEE_CAP_WEI,
|
||||
rpc_max_simulate_blocks: constants::DEFAULT_MAX_SIMULATE_BLOCKS,
|
||||
rpc_eth_proof_window: constants::DEFAULT_ETH_PROOF_WINDOW,
|
||||
gas_price_oracle: GasPriceOracleArgs::default(),
|
||||
@@ -422,4 +434,32 @@ mod tests {
|
||||
|
||||
assert_eq!(args, default_args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_tx_fee_cap_parse_integer() {
|
||||
let args = CommandParser::<RpcServerArgs>::parse_from(["reth", "--rpc.txfeecap", "2"]).args;
|
||||
let expected = 2_000_000_000_000_000_000u128; // 2 ETH in wei
|
||||
assert_eq!(args.rpc_tx_fee_cap, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_tx_fee_cap_parse_decimal() {
|
||||
let args =
|
||||
CommandParser::<RpcServerArgs>::parse_from(["reth", "--rpc.txfeecap", "1.5"]).args;
|
||||
let expected = 1_500_000_000_000_000_000u128; // 1.5 ETH in wei
|
||||
assert_eq!(args.rpc_tx_fee_cap, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_tx_fee_cap_parse_zero() {
|
||||
let args = CommandParser::<RpcServerArgs>::parse_from(["reth", "--rpc.txfeecap", "0"]).args;
|
||||
assert_eq!(args.rpc_tx_fee_cap, 0); // 0 = no cap
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_tx_fee_cap_parse_none() {
|
||||
let args = CommandParser::<RpcServerArgs>::parse_from(["reth"]).args;
|
||||
let expected = 1_000_000_000_000_000_000u128;
|
||||
assert_eq!(args.rpc_tx_fee_cap, expected); // 1 ETH default cap
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,6 +560,7 @@ where
|
||||
.no_eip4844()
|
||||
.with_head_timestamp(ctx.head().timestamp)
|
||||
.kzg_settings(ctx.kzg_settings()?)
|
||||
.set_tx_fee_cap(ctx.config().rpc.rpc_tx_fee_cap)
|
||||
.with_additional_tasks(
|
||||
pool_config_overrides
|
||||
.additional_validation_tasks
|
||||
|
||||
@@ -685,6 +685,15 @@ pub enum RpcPoolError {
|
||||
/// When the transaction exceeds the block gas limit
|
||||
#[error("exceeds block gas limit")]
|
||||
ExceedsGasLimit,
|
||||
/// Thrown when a new transaction is added to the pool, but then immediately discarded to
|
||||
/// respect the tx fee exceeds the configured cap
|
||||
#[error("tx fee ({max_tx_fee_wei} wei) exceeds the configured cap ({tx_fee_cap_wei} wei)")]
|
||||
ExceedsFeeCap {
|
||||
/// max fee in wei of new tx submitted to the pull (e.g. 0.11534 ETH)
|
||||
max_tx_fee_wei: u128,
|
||||
/// configured tx fee cap in wei (e.g. 1.0 ETH)
|
||||
tx_fee_cap_wei: u128,
|
||||
},
|
||||
/// When a negative value is encountered
|
||||
#[error("negative value")]
|
||||
NegativeValue,
|
||||
@@ -750,6 +759,9 @@ impl From<InvalidPoolTransactionError> for RpcPoolError {
|
||||
match err {
|
||||
InvalidPoolTransactionError::Consensus(err) => Self::Invalid(err.into()),
|
||||
InvalidPoolTransactionError::ExceedsGasLimit(_, _) => Self::ExceedsGasLimit,
|
||||
InvalidPoolTransactionError::ExceedsFeeCap { max_tx_fee_wei, tx_fee_cap_wei } => {
|
||||
Self::ExceedsFeeCap { max_tx_fee_wei, tx_fee_cap_wei }
|
||||
}
|
||||
InvalidPoolTransactionError::ExceedsMaxInitCodeSize(_, _) => {
|
||||
Self::ExceedsMaxInitCodeSize
|
||||
}
|
||||
|
||||
@@ -54,6 +54,9 @@ pub const DEFAULT_MAX_SIMULATE_BLOCKS: u64 = 256;
|
||||
/// The default eth historical proof window.
|
||||
pub const DEFAULT_ETH_PROOF_WINDOW: u64 = 0;
|
||||
|
||||
/// The default eth tx fee cap is 1 ETH
|
||||
pub const DEFAULT_TX_FEE_CAP_WEI: u128 = 1_000_000_000_000_000_000u128;
|
||||
|
||||
/// Maximum eth historical proof window. Equivalent to roughly 6 months of data on a 12
|
||||
/// second block time, and a month on a 2 second block time.
|
||||
pub const MAX_ETH_PROOF_WINDOW: u64 = 28 * 24 * 60 * 60 / 2;
|
||||
|
||||
@@ -200,6 +200,15 @@ pub enum InvalidPoolTransactionError {
|
||||
#[error("transaction's gas limit {0} exceeds block's gas limit {1}")]
|
||||
ExceedsGasLimit(u64, u64),
|
||||
/// Thrown when a new transaction is added to the pool, but then immediately discarded to
|
||||
/// respect the tx fee exceeds the configured cap
|
||||
#[error("tx fee ({max_tx_fee_wei} wei) exceeds the configured cap ({tx_fee_cap_wei} wei)")]
|
||||
ExceedsFeeCap {
|
||||
/// max fee in wei of new tx submitted to the pull (e.g. 0.11534 ETH)
|
||||
max_tx_fee_wei: u128,
|
||||
/// configured tx fee cap in wei (e.g. 1.0 ETH)
|
||||
tx_fee_cap_wei: u128,
|
||||
},
|
||||
/// Thrown when a new transaction is added to the pool, but then immediately discarded to
|
||||
/// respect the `max_init_code_size`.
|
||||
#[error("transaction's input size {0} exceeds max_init_code_size {1}")]
|
||||
ExceedsMaxInitCodeSize(usize, usize),
|
||||
@@ -287,6 +296,7 @@ impl InvalidPoolTransactionError {
|
||||
}
|
||||
}
|
||||
Self::ExceedsGasLimit(_, _) => true,
|
||||
Self::ExceedsFeeCap { max_tx_fee_wei: _, tx_fee_cap_wei: _ } => true,
|
||||
Self::ExceedsMaxInitCodeSize(_, _) => true,
|
||||
Self::OversizedData(_, _) => true,
|
||||
Self::Underpriced => {
|
||||
|
||||
@@ -168,6 +168,8 @@ pub(crate) struct EthTransactionValidatorInner<Client, T> {
|
||||
eip7702: bool,
|
||||
/// The current max gas limit
|
||||
block_gas_limit: AtomicU64,
|
||||
/// The current tx fee cap limit in wei locally submitted into the pool.
|
||||
tx_fee_cap: Option<u128>,
|
||||
/// Minimum priority fee to enforce for acceptance into the pool.
|
||||
minimum_priority_fee: Option<u128>,
|
||||
/// Stores the setup and parameters needed for validating KZG proofs.
|
||||
@@ -297,9 +299,35 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
// determine whether the transaction should be treated as local
|
||||
let is_local = self.local_transactions_config.is_local(origin, transaction.sender_ref());
|
||||
|
||||
// Ensure max possible transaction fee doesn't exceed configured transaction fee cap.
|
||||
// Only for transactions locally submitted for acceptance into the pool.
|
||||
if is_local {
|
||||
match self.tx_fee_cap {
|
||||
Some(0) | None => {} // Skip if cap is 0 or None
|
||||
Some(tx_fee_cap_wei) => {
|
||||
// max possible tx fee is (gas_price * gas_limit)
|
||||
// (if EIP1559) max possible tx fee is (max_fee_per_gas * gas_limit)
|
||||
let gas_price = transaction.max_fee_per_gas();
|
||||
let max_tx_fee_wei = gas_price.saturating_mul(transaction.gas_limit() as u128);
|
||||
if max_tx_fee_wei > tx_fee_cap_wei {
|
||||
return TransactionValidationOutcome::Invalid(
|
||||
transaction,
|
||||
InvalidPoolTransactionError::ExceedsFeeCap {
|
||||
max_tx_fee_wei,
|
||||
tx_fee_cap_wei,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drop non-local transactions with a fee lower than the configured fee for acceptance into
|
||||
// the pool.
|
||||
if !self.local_transactions_config.is_local(origin, transaction.sender_ref()) &&
|
||||
if !is_local &&
|
||||
transaction.is_dynamic_fee() &&
|
||||
transaction.max_priority_fee_per_gas() < self.minimum_priority_fee
|
||||
{
|
||||
@@ -590,6 +618,8 @@ pub struct EthTransactionValidatorBuilder<Client> {
|
||||
eip7702: bool,
|
||||
/// The current max gas limit
|
||||
block_gas_limit: AtomicU64,
|
||||
/// The current tx fee cap limit in wei locally submitted into the pool.
|
||||
tx_fee_cap: Option<u128>,
|
||||
/// Minimum priority fee to enforce for acceptance into the pool.
|
||||
minimum_priority_fee: Option<u128>,
|
||||
/// Determines how many additional tasks to spawn
|
||||
@@ -623,7 +653,7 @@ impl<Client> EthTransactionValidatorBuilder<Client> {
|
||||
kzg_settings: EnvKzgSettings::Default,
|
||||
local_transactions_config: Default::default(),
|
||||
max_tx_input_bytes: DEFAULT_MAX_TX_INPUT_BYTES,
|
||||
|
||||
tx_fee_cap: Some(1e18 as u128),
|
||||
// by default all transaction types are allowed
|
||||
eip2718: true,
|
||||
eip1559: true,
|
||||
@@ -770,6 +800,14 @@ impl<Client> EthTransactionValidatorBuilder<Client> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the block gas limit
|
||||
///
|
||||
/// Transactions with a gas limit greater than this will be rejected.
|
||||
pub fn set_tx_fee_cap(mut self, tx_fee_cap: u128) -> Self {
|
||||
self.tx_fee_cap = Some(tx_fee_cap);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds a the [`EthTransactionValidator`] without spawning validator tasks.
|
||||
pub fn build<Tx, S>(self, blob_store: S) -> EthTransactionValidator<Client, Tx>
|
||||
where
|
||||
@@ -785,6 +823,7 @@ impl<Client> EthTransactionValidatorBuilder<Client> {
|
||||
eip4844,
|
||||
eip7702,
|
||||
block_gas_limit,
|
||||
tx_fee_cap,
|
||||
minimum_priority_fee,
|
||||
kzg_settings,
|
||||
local_transactions_config,
|
||||
@@ -813,6 +852,7 @@ impl<Client> EthTransactionValidatorBuilder<Client> {
|
||||
eip4844,
|
||||
eip7702,
|
||||
block_gas_limit,
|
||||
tx_fee_cap,
|
||||
minimum_priority_fee,
|
||||
blob_store: Box::new(blob_store),
|
||||
kzg_settings,
|
||||
@@ -1035,4 +1075,77 @@ mod tests {
|
||||
let tx = pool.get(transaction.hash());
|
||||
assert!(tx.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_on_fee_cap_exceeded() {
|
||||
let transaction = get_transaction();
|
||||
let provider = MockEthProvider::default();
|
||||
provider.add_account(
|
||||
transaction.sender(),
|
||||
ExtendedAccount::new(transaction.nonce(), U256::MAX),
|
||||
);
|
||||
|
||||
let blob_store = InMemoryBlobStore::default();
|
||||
let validator = EthTransactionValidatorBuilder::new(provider)
|
||||
.set_tx_fee_cap(100) // 100 wei cap
|
||||
.build(blob_store.clone());
|
||||
|
||||
let outcome = validator.validate_one(TransactionOrigin::Local, transaction.clone());
|
||||
assert!(outcome.is_invalid());
|
||||
|
||||
if let TransactionValidationOutcome::Invalid(_, err) = outcome {
|
||||
assert!(matches!(
|
||||
err,
|
||||
InvalidPoolTransactionError::ExceedsFeeCap { max_tx_fee_wei, tx_fee_cap_wei }
|
||||
if (max_tx_fee_wei > tx_fee_cap_wei)
|
||||
));
|
||||
}
|
||||
|
||||
let pool =
|
||||
Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, Default::default());
|
||||
let res = pool.add_transaction(TransactionOrigin::Local, transaction.clone()).await;
|
||||
assert!(res.is_err());
|
||||
assert!(matches!(
|
||||
res.unwrap_err().kind,
|
||||
PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::ExceedsFeeCap { .. })
|
||||
));
|
||||
let tx = pool.get(transaction.hash());
|
||||
assert!(tx.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn valid_on_zero_fee_cap() {
|
||||
let transaction = get_transaction();
|
||||
let provider = MockEthProvider::default();
|
||||
provider.add_account(
|
||||
transaction.sender(),
|
||||
ExtendedAccount::new(transaction.nonce(), U256::MAX),
|
||||
);
|
||||
|
||||
let blob_store = InMemoryBlobStore::default();
|
||||
let validator = EthTransactionValidatorBuilder::new(provider)
|
||||
.set_tx_fee_cap(0) // no cap
|
||||
.build(blob_store);
|
||||
|
||||
let outcome = validator.validate_one(TransactionOrigin::Local, transaction);
|
||||
assert!(outcome.is_valid());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn valid_on_normal_fee_cap() {
|
||||
let transaction = get_transaction();
|
||||
let provider = MockEthProvider::default();
|
||||
provider.add_account(
|
||||
transaction.sender(),
|
||||
ExtendedAccount::new(transaction.nonce(), U256::MAX),
|
||||
);
|
||||
|
||||
let blob_store = InMemoryBlobStore::default();
|
||||
let validator = EthTransactionValidatorBuilder::new(provider)
|
||||
.set_tx_fee_cap(2e18 as u128) // 2 ETH cap
|
||||
.build(blob_store);
|
||||
|
||||
let outcome = validator.validate_one(TransactionOrigin::Local, transaction);
|
||||
assert!(outcome.is_valid());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user