Skip to content

Commit 2aa992a

Browse files
authored
feat(anvil): add option to disable pool balance checks in EVM configuration (#11242)
* feat(anvil): add option to disable pool balance checks in EVM configuration * test(anvil): add test for transaction pool behavior with disabled balance checks * fix: group pool balance checks for clarity * fix: make clippy happy again :)
1 parent fbd7b1f commit 2aa992a

File tree

4 files changed

+153
-69
lines changed

4 files changed

+153
-69
lines changed

crates/anvil/src/cmd.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ impl NodeArgs {
280280
.with_optimism(self.evm.optimism)
281281
.with_odyssey(self.evm.odyssey)
282282
.with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer)
283+
.with_disable_pool_balance_checks(self.evm.disable_pool_balance_checks)
283284
.with_slots_in_an_epoch(self.slots_in_an_epoch)
284285
.with_memory_limit(self.evm.memory_limit)
285286
.with_cache_path(self.cache_path))
@@ -592,6 +593,10 @@ pub struct AnvilEvmArgs {
592593
#[arg(long, visible_alias = "no-create2")]
593594
pub disable_default_create2_deployer: bool,
594595

596+
/// Disable pool balance checks
597+
#[arg(long)]
598+
pub disable_pool_balance_checks: bool,
599+
595600
/// The memory limit per EVM execution in bytes.
596601
#[arg(long)]
597602
pub memory_limit: Option<u64>,

crates/anvil/src/config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ pub struct NodeConfig {
184184
pub transaction_block_keeper: Option<usize>,
185185
/// Disable the default CREATE2 deployer
186186
pub disable_default_create2_deployer: bool,
187+
/// Disable pool balance checks
188+
pub disable_pool_balance_checks: bool,
187189
/// Enable Optimism deposit transaction
188190
pub enable_optimism: bool,
189191
/// Slots in an epoch
@@ -484,6 +486,7 @@ impl Default for NodeConfig {
484486
init_state: None,
485487
transaction_block_keeper: None,
486488
disable_default_create2_deployer: false,
489+
disable_pool_balance_checks: false,
487490
enable_optimism: false,
488491
slots_in_an_epoch: 32,
489492
memory_limit: None,
@@ -993,6 +996,13 @@ impl NodeConfig {
993996
self
994997
}
995998

999+
/// Sets whether to disable pool balance checks
1000+
#[must_use]
1001+
pub fn with_disable_pool_balance_checks(mut self, yes: bool) -> Self {
1002+
self.disable_pool_balance_checks = yes;
1003+
self
1004+
}
1005+
9961006
/// Injects precompiles to `anvil`'s EVM.
9971007
#[must_use]
9981008
pub fn with_precompile_factory(mut self, factory: impl PrecompileFactory + 'static) -> Self {

crates/anvil/src/eth/backend/mem/mod.rs

Lines changed: 77 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ pub struct Backend {
246246
// === wallet === //
247247
capabilities: Arc<RwLock<WalletCapabilities>>,
248248
executor_wallet: Arc<RwLock<Option<EthereumWallet>>>,
249+
/// Disable pool balance checks
250+
disable_pool_balance_checks: bool,
249251
}
250252

251253
impl Backend {
@@ -309,9 +311,9 @@ impl Backend {
309311
states = states.disk_path(cache_path);
310312
}
311313

312-
let (slots_in_an_epoch, precompile_factory) = {
314+
let (slots_in_an_epoch, precompile_factory, disable_pool_balance_checks) = {
313315
let cfg = node_config.read().await;
314-
(cfg.slots_in_an_epoch, cfg.precompile_factory.clone())
316+
(cfg.slots_in_an_epoch, cfg.precompile_factory.clone(), cfg.disable_pool_balance_checks)
315317
};
316318

317319
let (capabilities, executor_wallet) = if odyssey {
@@ -376,6 +378,7 @@ impl Backend {
376378
mining: Arc::new(tokio::sync::Mutex::new(())),
377379
capabilities: Arc::new(RwLock::new(capabilities)),
378380
executor_wallet: Arc::new(RwLock::new(executor_wallet)),
381+
disable_pool_balance_checks,
379382
};
380383

381384
if let Some(interval_block_time) = automine_block_time {
@@ -3341,22 +3344,7 @@ impl TransactionValidator for Backend {
33413344
}
33423345
}
33433346

3344-
if tx.gas_limit() < MIN_TRANSACTION_GAS as u64 {
3345-
warn!(target: "backend", "[{:?}] gas too low", tx.hash());
3346-
return Err(InvalidTransactionError::GasTooLow);
3347-
}
3348-
3349-
// Check gas limit, iff block gas limit is set.
3350-
if !env.evm_env.cfg_env.disable_block_gas_limit
3351-
&& tx.gas_limit() > env.evm_env.block_env.gas_limit
3352-
{
3353-
warn!(target: "backend", "[{:?}] gas too high", tx.hash());
3354-
return Err(InvalidTransactionError::GasTooHigh(ErrDetail {
3355-
detail: String::from("tx.gas_limit > env.block.gas_limit"),
3356-
}));
3357-
}
3358-
3359-
// check nonce
3347+
// Nonce validation
33603348
let is_deposit_tx =
33613349
matches!(&pending.transaction.transaction, TypedTransaction::Deposit(_));
33623350
let nonce = tx.nonce();
@@ -3365,39 +3353,15 @@ impl TransactionValidator for Backend {
33653353
return Err(InvalidTransactionError::NonceTooLow);
33663354
}
33673355

3368-
if env.evm_env.cfg_env.spec >= SpecId::LONDON {
3369-
if tx.gas_price() < env.evm_env.block_env.basefee.into() && !is_deposit_tx {
3370-
warn!(target: "backend", "max fee per gas={}, too low, block basefee={}",tx.gas_price(), env.evm_env.block_env.basefee);
3371-
return Err(InvalidTransactionError::FeeCapTooLow);
3372-
}
3373-
3374-
if let (Some(max_priority_fee_per_gas), Some(max_fee_per_gas)) =
3375-
(tx.essentials().max_priority_fee_per_gas, tx.essentials().max_fee_per_gas)
3376-
&& max_priority_fee_per_gas > max_fee_per_gas
3377-
{
3378-
warn!(target: "backend", "max priority fee per gas={}, too high, max fee per gas={}", max_priority_fee_per_gas, max_fee_per_gas);
3379-
return Err(InvalidTransactionError::TipAboveFeeCap);
3380-
}
3381-
}
3382-
3383-
// EIP-4844 Cancun hard fork validation steps
3356+
// EIP-4844 structural validation
33843357
if env.evm_env.cfg_env.spec >= SpecId::CANCUN && tx.transaction.is_eip4844() {
3385-
// Light checks first: see if the blob fee cap is too low.
3386-
if let Some(max_fee_per_blob_gas) = tx.essentials().max_fee_per_blob_gas
3387-
&& let Some(blob_gas_and_price) = &env.evm_env.block_env.blob_excess_gas_and_price
3388-
&& max_fee_per_blob_gas < blob_gas_and_price.blob_gasprice
3389-
{
3390-
warn!(target: "backend", "max fee per blob gas={}, too low, block blob gas price={}", max_fee_per_blob_gas, blob_gas_and_price.blob_gasprice);
3391-
return Err(InvalidTransactionError::BlobFeeCapTooLow);
3392-
}
3393-
33943358
// Heavy (blob validation) checks
3395-
let tx = match &tx.transaction {
3359+
let blob_tx = match &tx.transaction {
33963360
TypedTransaction::EIP4844(tx) => tx.tx(),
33973361
_ => unreachable!(),
33983362
};
33993363

3400-
let blob_count = tx.tx().blob_versioned_hashes.len();
3364+
let blob_count = blob_tx.tx().blob_versioned_hashes.len();
34013365

34023366
// Ensure there are blob hashes.
34033367
if blob_count == 0 {
@@ -3412,40 +3376,85 @@ impl TransactionValidator for Backend {
34123376

34133377
// Check for any blob validation errors if not impersonating.
34143378
if !self.skip_blob_validation(Some(*pending.sender()))
3415-
&& let Err(err) = tx.validate(EnvKzgSettings::default().get())
3379+
&& let Err(err) = blob_tx.validate(EnvKzgSettings::default().get())
34163380
{
34173381
return Err(InvalidTransactionError::BlobTransactionValidationError(err));
34183382
}
34193383
}
34203384

3421-
let max_cost = tx.max_cost();
3422-
let value = tx.value();
3385+
// Balance and fee related checks
3386+
if !self.disable_pool_balance_checks {
3387+
// Gas limit validation
3388+
if tx.gas_limit() < MIN_TRANSACTION_GAS as u64 {
3389+
warn!(target: "backend", "[{:?}] gas too low", tx.hash());
3390+
return Err(InvalidTransactionError::GasTooLow);
3391+
}
34233392

3424-
match &tx.transaction {
3425-
TypedTransaction::Deposit(deposit_tx) => {
3426-
// Deposit transactions
3427-
// https://specs.optimism.io/protocol/deposits.html#execution
3428-
// 1. no gas cost check required since already have prepaid gas from L1
3429-
// 2. increment account balance by deposited amount before checking for sufficient
3430-
// funds `tx.value <= existing account value + deposited value`
3431-
if value > account.balance + U256::from(deposit_tx.mint) {
3432-
warn!(target: "backend", "[{:?}] insufficient balance={}, required={} account={:?}", tx.hash(), account.balance + U256::from(deposit_tx.mint), value, *pending.sender());
3433-
return Err(InvalidTransactionError::InsufficientFunds);
3393+
// Check gas limit against block gas limit, if block gas limit is set.
3394+
if !env.evm_env.cfg_env.disable_block_gas_limit
3395+
&& tx.gas_limit() > env.evm_env.block_env.gas_limit
3396+
{
3397+
warn!(target: "backend", "[{:?}] gas too high", tx.hash());
3398+
return Err(InvalidTransactionError::GasTooHigh(ErrDetail {
3399+
detail: String::from("tx.gas_limit > env.block.gas_limit"),
3400+
}));
3401+
}
3402+
3403+
// EIP-1559 fee validation (London hard fork and later)
3404+
if env.evm_env.cfg_env.spec >= SpecId::LONDON {
3405+
if tx.gas_price() < env.evm_env.block_env.basefee.into() && !is_deposit_tx {
3406+
warn!(target: "backend", "max fee per gas={}, too low, block basefee={}",tx.gas_price(), env.evm_env.block_env.basefee);
3407+
return Err(InvalidTransactionError::FeeCapTooLow);
34343408
}
3409+
3410+
if let (Some(max_priority_fee_per_gas), Some(max_fee_per_gas)) =
3411+
(tx.essentials().max_priority_fee_per_gas, tx.essentials().max_fee_per_gas)
3412+
&& max_priority_fee_per_gas > max_fee_per_gas
3413+
{
3414+
warn!(target: "backend", "max priority fee per gas={}, too high, max fee per gas={}", max_priority_fee_per_gas, max_fee_per_gas);
3415+
return Err(InvalidTransactionError::TipAboveFeeCap);
3416+
}
3417+
}
3418+
3419+
// EIP-4844 blob fee validation
3420+
if env.evm_env.cfg_env.spec >= SpecId::CANCUN
3421+
&& tx.transaction.is_eip4844()
3422+
&& let Some(max_fee_per_blob_gas) = tx.essentials().max_fee_per_blob_gas
3423+
&& let Some(blob_gas_and_price) = &env.evm_env.block_env.blob_excess_gas_and_price
3424+
&& max_fee_per_blob_gas < blob_gas_and_price.blob_gasprice
3425+
{
3426+
warn!(target: "backend", "max fee per blob gas={}, too low, block blob gas price={}", max_fee_per_blob_gas, blob_gas_and_price.blob_gasprice);
3427+
return Err(InvalidTransactionError::BlobFeeCapTooLow);
34353428
}
3436-
_ => {
3437-
// check sufficient funds: `gas * price + value`
3438-
let req_funds = max_cost.checked_add(value.saturating_to()).ok_or_else(|| {
3439-
warn!(target: "backend", "[{:?}] cost too high", tx.hash());
3440-
InvalidTransactionError::InsufficientFunds
3441-
})?;
3442-
if account.balance < U256::from(req_funds) {
3443-
warn!(target: "backend", "[{:?}] insufficient allowance={}, required={} account={:?}", tx.hash(), account.balance, req_funds, *pending.sender());
3444-
return Err(InvalidTransactionError::InsufficientFunds);
3429+
3430+
let max_cost = tx.max_cost();
3431+
let value = tx.value();
3432+
match &tx.transaction {
3433+
TypedTransaction::Deposit(deposit_tx) => {
3434+
// Deposit transactions
3435+
// https://specs.optimism.io/protocol/deposits.html#execution
3436+
// 1. no gas cost check required since already have prepaid gas from L1
3437+
// 2. increment account balance by deposited amount before checking for
3438+
// sufficient funds `tx.value <= existing account value + deposited value`
3439+
if value > account.balance + U256::from(deposit_tx.mint) {
3440+
warn!(target: "backend", "[{:?}] insufficient balance={}, required={} account={:?}", tx.hash(), account.balance + U256::from(deposit_tx.mint), value, *pending.sender());
3441+
return Err(InvalidTransactionError::InsufficientFunds);
3442+
}
3443+
}
3444+
_ => {
3445+
// check sufficient funds: `gas * price + value`
3446+
let req_funds =
3447+
max_cost.checked_add(value.saturating_to()).ok_or_else(|| {
3448+
warn!(target: "backend", "[{:?}] cost too high", tx.hash());
3449+
InvalidTransactionError::InsufficientFunds
3450+
})?;
3451+
if account.balance < U256::from(req_funds) {
3452+
warn!(target: "backend", "[{:?}] insufficient allowance={}, required={} account={:?}", tx.hash(), account.balance, req_funds, *pending.sender());
3453+
return Err(InvalidTransactionError::InsufficientFunds);
3454+
}
34453455
}
34463456
}
34473457
}
3448-
34493458
Ok(())
34503459
}
34513460

crates/anvil/tests/it/txpool.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! txpool related tests
22
3-
use alloy_network::TransactionBuilder;
3+
use alloy_network::{ReceiptResponse, TransactionBuilder};
44
use alloy_primitives::U256;
55
use alloy_provider::{Provider, ext::TxPoolApi};
66
use alloy_rpc_types::TransactionRequest;
@@ -54,3 +54,63 @@ async fn geth_txpool() {
5454
assert!(content.contains_key(&nonce.to_string()));
5555
}
5656
}
57+
58+
// Cf. https://github.com/foundry-rs/foundry/issues/11239
59+
#[tokio::test(flavor = "multi_thread")]
60+
async fn accepts_spend_after_funding_when_pool_checks_disabled() {
61+
// Spawn with pool balance checks disabled
62+
let (api, handle) = spawn(NodeConfig::test().with_disable_pool_balance_checks(true)).await;
63+
let provider = handle.http_provider();
64+
65+
// Work with pending pool (no automine)
66+
api.anvil_set_auto_mine(false).await.unwrap();
67+
68+
// Funder is a dev account controlled by the node
69+
let funder = provider.get_accounts().await.unwrap().remove(0);
70+
71+
// Recipient/spender is a random address with zero balance that we'll impersonate
72+
let spender = alloy_primitives::Address::random();
73+
api.anvil_set_balance(spender, U256::from(0u64)).await.unwrap();
74+
api.anvil_impersonate_account(spender).await.unwrap();
75+
76+
// Ensure tx1 (funding) has higher gas price so it's mined before tx2 within the same block
77+
let gas_price_fund = 2_000_000_000_000u128; // 2_000 gwei
78+
let gas_price_spend = 1_000_000_000u128; // 1 gwei
79+
80+
let fund_value = U256::from(1_000_000_000_000_000_000u128); // 1 ether
81+
82+
// tx1: fund spender from funder
83+
let tx1 = TransactionRequest::default()
84+
.with_from(funder)
85+
.with_to(spender)
86+
.with_value(fund_value)
87+
.with_gas_price(gas_price_fund);
88+
let tx1 = WithOtherFields::new(tx1);
89+
90+
// tx2: spender attempts to send value greater than their pre-funding balance (0),
91+
// which would normally be rejected by pool balance checks, but should be accepted when disabled
92+
let spend_value = fund_value - U256::from(21_000u64) * U256::from(gas_price_spend);
93+
let tx2 = TransactionRequest::default()
94+
.with_from(spender)
95+
.with_to(funder)
96+
.with_value(spend_value)
97+
.with_gas_price(gas_price_spend);
98+
let tx2 = WithOtherFields::new(tx2);
99+
100+
// Publish both transactions (funding first, then spend-before-funding-is-mined)
101+
let sent1 = provider.send_transaction(tx1).await.unwrap();
102+
let sent2 = provider.send_transaction(tx2).await.unwrap();
103+
104+
// Both should be accepted into the pool (pending)
105+
let status = provider.txpool_status().await.unwrap();
106+
assert_eq!(status.pending, 2);
107+
assert_eq!(status.queued, 0);
108+
109+
// Mine a block and ensure both succeed
110+
api.evm_mine(None).await.unwrap();
111+
112+
let receipt1 = sent1.get_receipt().await.unwrap();
113+
let receipt2 = sent2.get_receipt().await.unwrap();
114+
assert!(receipt1.status());
115+
assert!(receipt2.status());
116+
}

0 commit comments

Comments
 (0)