Skip to content

Commit 4850bd5

Browse files
rrrengineermattsse
andauthored
feat: adding cli --rpc.txfeecap flag (#15654)
Co-authored-by: Matthias Seitz <[email protected]>
1 parent a217696 commit 4850bd5

File tree

10 files changed

+226
-3
lines changed

10 files changed

+226
-3
lines changed

book/cli/reth/node.md

+5
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,11 @@ RPC:
362362
363363
[default: 50000000]
364364
365+
--rpc.txfeecap <TX_FEE_CAP>
366+
Maximum eth transaction fee that can be sent via the RPC APIs (0 = no cap)
367+
368+
[default: 1.0]
369+
365370
--rpc.max-simulate-blocks <BLOCKS_COUNT>
366371
Maximum number of blocks for `eth_simulateV1` call
367372

crates/cli/util/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub use load_secret_key::get_secret_key;
1818
pub mod parsers;
1919
pub use parsers::{
2020
hash_or_num_value_parser, parse_duration_from_secs, parse_duration_from_secs_or_ms,
21-
parse_socket_address,
21+
parse_ether_value, parse_socket_address,
2222
};
2323

2424
#[cfg(all(unix, any(target_env = "gnu", target_os = "macos")))]

crates/cli/util/src/parsers.rs

+38
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,23 @@ pub fn read_json_from_file<T: serde::de::DeserializeOwned>(path: &str) -> Result
8989
reth_fs_util::read_json_file(Path::new(path))
9090
}
9191

92+
/// Parses an ether value from a string.
93+
///
94+
/// The amount in eth like "1.05" will be interpreted in wei (1.05 * 1e18).
95+
/// Supports both decimal and integer inputs.
96+
///
97+
/// # Examples
98+
/// - "1.05" -> 1.05 ETH = 1.05 * 10^18 wei
99+
/// - "2" -> 2 ETH = 2 * 10^18 wei
100+
pub fn parse_ether_value(value: &str) -> eyre::Result<u128> {
101+
let eth = value.parse::<f64>()?;
102+
if eth.is_sign_negative() {
103+
return Err(eyre::eyre!("Ether value cannot be negative"))
104+
}
105+
let wei = eth * 1e18;
106+
Ok(wei as u128)
107+
}
108+
92109
#[cfg(test)]
93110
mod tests {
94111
use super::*;
@@ -131,4 +148,25 @@ mod tests {
131148

132149
assert!(parse_duration_from_secs_or_ms("5ns").is_err());
133150
}
151+
152+
#[test]
153+
fn parse_ether_values() {
154+
// Test basic decimal value
155+
let wei = parse_ether_value("1.05").unwrap();
156+
assert_eq!(wei, 1_050_000_000_000_000_000u128);
157+
158+
// Test integer value
159+
let wei = parse_ether_value("2").unwrap();
160+
assert_eq!(wei, 2_000_000_000_000_000_000u128);
161+
162+
// Test zero
163+
let wei = parse_ether_value("0").unwrap();
164+
assert_eq!(wei, 0);
165+
166+
// Test negative value fails
167+
assert!(parse_ether_value("-1").is_err());
168+
169+
// Test invalid input fails
170+
assert!(parse_ether_value("abc").is_err());
171+
}
134172
}

crates/ethereum/node/src/node.rs

+1
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ where
363363
.with_head_timestamp(ctx.head().timestamp)
364364
.kzg_settings(ctx.kzg_settings()?)
365365
.with_local_transactions_config(pool_config.local_transactions_config.clone())
366+
.set_tx_fee_cap(ctx.config().rpc.rpc_tx_fee_cap)
366367
.with_additional_tasks(ctx.config().txpool.additional_validation_tasks)
367368
.build_with_tasks(ctx.task_executor().clone(), blob_store.clone());
368369

crates/node/core/src/args/rpc_server.rs

+40
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use clap::{
1414
Arg, Args, Command,
1515
};
1616
use rand::Rng;
17+
use reth_cli_util::parse_ether_value;
1718
use reth_rpc_server_types::{constants, RethRpcModule, RpcModuleSelection};
1819

1920
use crate::args::{
@@ -169,6 +170,16 @@ pub struct RpcServerArgs {
169170
)]
170171
pub rpc_gas_cap: u64,
171172

173+
/// Maximum eth transaction fee that can be sent via the RPC APIs (0 = no cap)
174+
#[arg(
175+
long = "rpc.txfeecap",
176+
alias = "rpc-txfeecap",
177+
value_name = "TX_FEE_CAP",
178+
value_parser = parse_ether_value,
179+
default_value = "1.0"
180+
)]
181+
pub rpc_tx_fee_cap: u128,
182+
172183
/// Maximum number of blocks for `eth_simulateV1` call.
173184
#[arg(
174185
long = "rpc.max-simulate-blocks",
@@ -329,6 +340,7 @@ impl Default for RpcServerArgs {
329340
rpc_max_blocks_per_filter: constants::DEFAULT_MAX_BLOCKS_PER_FILTER.into(),
330341
rpc_max_logs_per_response: (constants::DEFAULT_MAX_LOGS_PER_RESPONSE as u64).into(),
331342
rpc_gas_cap: constants::gas_oracle::RPC_DEFAULT_GAS_CAP,
343+
rpc_tx_fee_cap: constants::DEFAULT_TX_FEE_CAP_WEI,
332344
rpc_max_simulate_blocks: constants::DEFAULT_MAX_SIMULATE_BLOCKS,
333345
rpc_eth_proof_window: constants::DEFAULT_ETH_PROOF_WINDOW,
334346
gas_price_oracle: GasPriceOracleArgs::default(),
@@ -422,4 +434,32 @@ mod tests {
422434

423435
assert_eq!(args, default_args);
424436
}
437+
438+
#[test]
439+
fn test_rpc_tx_fee_cap_parse_integer() {
440+
let args = CommandParser::<RpcServerArgs>::parse_from(["reth", "--rpc.txfeecap", "2"]).args;
441+
let expected = 2_000_000_000_000_000_000u128; // 2 ETH in wei
442+
assert_eq!(args.rpc_tx_fee_cap, expected);
443+
}
444+
445+
#[test]
446+
fn test_rpc_tx_fee_cap_parse_decimal() {
447+
let args =
448+
CommandParser::<RpcServerArgs>::parse_from(["reth", "--rpc.txfeecap", "1.5"]).args;
449+
let expected = 1_500_000_000_000_000_000u128; // 1.5 ETH in wei
450+
assert_eq!(args.rpc_tx_fee_cap, expected);
451+
}
452+
453+
#[test]
454+
fn test_rpc_tx_fee_cap_parse_zero() {
455+
let args = CommandParser::<RpcServerArgs>::parse_from(["reth", "--rpc.txfeecap", "0"]).args;
456+
assert_eq!(args.rpc_tx_fee_cap, 0); // 0 = no cap
457+
}
458+
459+
#[test]
460+
fn test_rpc_tx_fee_cap_parse_none() {
461+
let args = CommandParser::<RpcServerArgs>::parse_from(["reth"]).args;
462+
let expected = 1_000_000_000_000_000_000u128;
463+
assert_eq!(args.rpc_tx_fee_cap, expected); // 1 ETH default cap
464+
}
425465
}

crates/optimism/node/src/node.rs

+1
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@ where
560560
.no_eip4844()
561561
.with_head_timestamp(ctx.head().timestamp)
562562
.kzg_settings(ctx.kzg_settings()?)
563+
.set_tx_fee_cap(ctx.config().rpc.rpc_tx_fee_cap)
563564
.with_additional_tasks(
564565
pool_config_overrides
565566
.additional_validation_tasks

crates/rpc/rpc-eth-types/src/error/mod.rs

+12
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,15 @@ pub enum RpcPoolError {
685685
/// When the transaction exceeds the block gas limit
686686
#[error("exceeds block gas limit")]
687687
ExceedsGasLimit,
688+
/// Thrown when a new transaction is added to the pool, but then immediately discarded to
689+
/// respect the tx fee exceeds the configured cap
690+
#[error("tx fee ({max_tx_fee_wei} wei) exceeds the configured cap ({tx_fee_cap_wei} wei)")]
691+
ExceedsFeeCap {
692+
/// max fee in wei of new tx submitted to the pull (e.g. 0.11534 ETH)
693+
max_tx_fee_wei: u128,
694+
/// configured tx fee cap in wei (e.g. 1.0 ETH)
695+
tx_fee_cap_wei: u128,
696+
},
688697
/// When a negative value is encountered
689698
#[error("negative value")]
690699
NegativeValue,
@@ -750,6 +759,9 @@ impl From<InvalidPoolTransactionError> for RpcPoolError {
750759
match err {
751760
InvalidPoolTransactionError::Consensus(err) => Self::Invalid(err.into()),
752761
InvalidPoolTransactionError::ExceedsGasLimit(_, _) => Self::ExceedsGasLimit,
762+
InvalidPoolTransactionError::ExceedsFeeCap { max_tx_fee_wei, tx_fee_cap_wei } => {
763+
Self::ExceedsFeeCap { max_tx_fee_wei, tx_fee_cap_wei }
764+
}
753765
InvalidPoolTransactionError::ExceedsMaxInitCodeSize(_, _) => {
754766
Self::ExceedsMaxInitCodeSize
755767
}

crates/rpc/rpc-server-types/src/constants.rs

+3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ pub const DEFAULT_MAX_SIMULATE_BLOCKS: u64 = 256;
5454
/// The default eth historical proof window.
5555
pub const DEFAULT_ETH_PROOF_WINDOW: u64 = 0;
5656

57+
/// The default eth tx fee cap is 1 ETH
58+
pub const DEFAULT_TX_FEE_CAP_WEI: u128 = 1_000_000_000_000_000_000u128;
59+
5760
/// Maximum eth historical proof window. Equivalent to roughly 6 months of data on a 12
5861
/// second block time, and a month on a 2 second block time.
5962
pub const MAX_ETH_PROOF_WINDOW: u64 = 28 * 24 * 60 * 60 / 2;

crates/transaction-pool/src/error.rs

+10
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,15 @@ pub enum InvalidPoolTransactionError {
200200
#[error("transaction's gas limit {0} exceeds block's gas limit {1}")]
201201
ExceedsGasLimit(u64, u64),
202202
/// Thrown when a new transaction is added to the pool, but then immediately discarded to
203+
/// respect the tx fee exceeds the configured cap
204+
#[error("tx fee ({max_tx_fee_wei} wei) exceeds the configured cap ({tx_fee_cap_wei} wei)")]
205+
ExceedsFeeCap {
206+
/// max fee in wei of new tx submitted to the pull (e.g. 0.11534 ETH)
207+
max_tx_fee_wei: u128,
208+
/// configured tx fee cap in wei (e.g. 1.0 ETH)
209+
tx_fee_cap_wei: u128,
210+
},
211+
/// Thrown when a new transaction is added to the pool, but then immediately discarded to
203212
/// respect the `max_init_code_size`.
204213
#[error("transaction's input size {0} exceeds max_init_code_size {1}")]
205214
ExceedsMaxInitCodeSize(usize, usize),
@@ -287,6 +296,7 @@ impl InvalidPoolTransactionError {
287296
}
288297
}
289298
Self::ExceedsGasLimit(_, _) => true,
299+
Self::ExceedsFeeCap { max_tx_fee_wei: _, tx_fee_cap_wei: _ } => true,
290300
Self::ExceedsMaxInitCodeSize(_, _) => true,
291301
Self::OversizedData(_, _) => true,
292302
Self::Underpriced => {

crates/transaction-pool/src/validate/eth.rs

+115-2
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ pub(crate) struct EthTransactionValidatorInner<Client, T> {
168168
eip7702: bool,
169169
/// The current max gas limit
170170
block_gas_limit: AtomicU64,
171+
/// The current tx fee cap limit in wei locally submitted into the pool.
172+
tx_fee_cap: Option<u128>,
171173
/// Minimum priority fee to enforce for acceptance into the pool.
172174
minimum_priority_fee: Option<u128>,
173175
/// Stores the setup and parameters needed for validating KZG proofs.
@@ -297,9 +299,35 @@ where
297299
)
298300
}
299301

302+
// determine whether the transaction should be treated as local
303+
let is_local = self.local_transactions_config.is_local(origin, transaction.sender_ref());
304+
305+
// Ensure max possible transaction fee doesn't exceed configured transaction fee cap.
306+
// Only for transactions locally submitted for acceptance into the pool.
307+
if is_local {
308+
match self.tx_fee_cap {
309+
Some(0) | None => {} // Skip if cap is 0 or None
310+
Some(tx_fee_cap_wei) => {
311+
// max possible tx fee is (gas_price * gas_limit)
312+
// (if EIP1559) max possible tx fee is (max_fee_per_gas * gas_limit)
313+
let gas_price = transaction.max_fee_per_gas();
314+
let max_tx_fee_wei = gas_price.saturating_mul(transaction.gas_limit() as u128);
315+
if max_tx_fee_wei > tx_fee_cap_wei {
316+
return TransactionValidationOutcome::Invalid(
317+
transaction,
318+
InvalidPoolTransactionError::ExceedsFeeCap {
319+
max_tx_fee_wei,
320+
tx_fee_cap_wei,
321+
},
322+
);
323+
}
324+
}
325+
}
326+
}
327+
300328
// Drop non-local transactions with a fee lower than the configured fee for acceptance into
301329
// the pool.
302-
if !self.local_transactions_config.is_local(origin, transaction.sender_ref()) &&
330+
if !is_local &&
303331
transaction.is_dynamic_fee() &&
304332
transaction.max_priority_fee_per_gas() < self.minimum_priority_fee
305333
{
@@ -590,6 +618,8 @@ pub struct EthTransactionValidatorBuilder<Client> {
590618
eip7702: bool,
591619
/// The current max gas limit
592620
block_gas_limit: AtomicU64,
621+
/// The current tx fee cap limit in wei locally submitted into the pool.
622+
tx_fee_cap: Option<u128>,
593623
/// Minimum priority fee to enforce for acceptance into the pool.
594624
minimum_priority_fee: Option<u128>,
595625
/// Determines how many additional tasks to spawn
@@ -623,7 +653,7 @@ impl<Client> EthTransactionValidatorBuilder<Client> {
623653
kzg_settings: EnvKzgSettings::Default,
624654
local_transactions_config: Default::default(),
625655
max_tx_input_bytes: DEFAULT_MAX_TX_INPUT_BYTES,
626-
656+
tx_fee_cap: Some(1e18 as u128),
627657
// by default all transaction types are allowed
628658
eip2718: true,
629659
eip1559: true,
@@ -770,6 +800,14 @@ impl<Client> EthTransactionValidatorBuilder<Client> {
770800
self
771801
}
772802

803+
/// Sets the block gas limit
804+
///
805+
/// Transactions with a gas limit greater than this will be rejected.
806+
pub fn set_tx_fee_cap(mut self, tx_fee_cap: u128) -> Self {
807+
self.tx_fee_cap = Some(tx_fee_cap);
808+
self
809+
}
810+
773811
/// Builds a the [`EthTransactionValidator`] without spawning validator tasks.
774812
pub fn build<Tx, S>(self, blob_store: S) -> EthTransactionValidator<Client, Tx>
775813
where
@@ -785,6 +823,7 @@ impl<Client> EthTransactionValidatorBuilder<Client> {
785823
eip4844,
786824
eip7702,
787825
block_gas_limit,
826+
tx_fee_cap,
788827
minimum_priority_fee,
789828
kzg_settings,
790829
local_transactions_config,
@@ -813,6 +852,7 @@ impl<Client> EthTransactionValidatorBuilder<Client> {
813852
eip4844,
814853
eip7702,
815854
block_gas_limit,
855+
tx_fee_cap,
816856
minimum_priority_fee,
817857
blob_store: Box::new(blob_store),
818858
kzg_settings,
@@ -1035,4 +1075,77 @@ mod tests {
10351075
let tx = pool.get(transaction.hash());
10361076
assert!(tx.is_none());
10371077
}
1078+
1079+
#[tokio::test]
1080+
async fn invalid_on_fee_cap_exceeded() {
1081+
let transaction = get_transaction();
1082+
let provider = MockEthProvider::default();
1083+
provider.add_account(
1084+
transaction.sender(),
1085+
ExtendedAccount::new(transaction.nonce(), U256::MAX),
1086+
);
1087+
1088+
let blob_store = InMemoryBlobStore::default();
1089+
let validator = EthTransactionValidatorBuilder::new(provider)
1090+
.set_tx_fee_cap(100) // 100 wei cap
1091+
.build(blob_store.clone());
1092+
1093+
let outcome = validator.validate_one(TransactionOrigin::Local, transaction.clone());
1094+
assert!(outcome.is_invalid());
1095+
1096+
if let TransactionValidationOutcome::Invalid(_, err) = outcome {
1097+
assert!(matches!(
1098+
err,
1099+
InvalidPoolTransactionError::ExceedsFeeCap { max_tx_fee_wei, tx_fee_cap_wei }
1100+
if (max_tx_fee_wei > tx_fee_cap_wei)
1101+
));
1102+
}
1103+
1104+
let pool =
1105+
Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, Default::default());
1106+
let res = pool.add_transaction(TransactionOrigin::Local, transaction.clone()).await;
1107+
assert!(res.is_err());
1108+
assert!(matches!(
1109+
res.unwrap_err().kind,
1110+
PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::ExceedsFeeCap { .. })
1111+
));
1112+
let tx = pool.get(transaction.hash());
1113+
assert!(tx.is_none());
1114+
}
1115+
1116+
#[tokio::test]
1117+
async fn valid_on_zero_fee_cap() {
1118+
let transaction = get_transaction();
1119+
let provider = MockEthProvider::default();
1120+
provider.add_account(
1121+
transaction.sender(),
1122+
ExtendedAccount::new(transaction.nonce(), U256::MAX),
1123+
);
1124+
1125+
let blob_store = InMemoryBlobStore::default();
1126+
let validator = EthTransactionValidatorBuilder::new(provider)
1127+
.set_tx_fee_cap(0) // no cap
1128+
.build(blob_store);
1129+
1130+
let outcome = validator.validate_one(TransactionOrigin::Local, transaction);
1131+
assert!(outcome.is_valid());
1132+
}
1133+
1134+
#[tokio::test]
1135+
async fn valid_on_normal_fee_cap() {
1136+
let transaction = get_transaction();
1137+
let provider = MockEthProvider::default();
1138+
provider.add_account(
1139+
transaction.sender(),
1140+
ExtendedAccount::new(transaction.nonce(), U256::MAX),
1141+
);
1142+
1143+
let blob_store = InMemoryBlobStore::default();
1144+
let validator = EthTransactionValidatorBuilder::new(provider)
1145+
.set_tx_fee_cap(2e18 as u128) // 2 ETH cap
1146+
.build(blob_store);
1147+
1148+
let outcome = validator.validate_one(TransactionOrigin::Local, transaction);
1149+
assert!(outcome.is_valid());
1150+
}
10381151
}

0 commit comments

Comments
 (0)