Skip to content

Commit 9405989

Browse files
MartinquaXDgithub-actions-bot
authored andcommitted
debounce gas price rpc requests (#4573)
# Description When we switched from the old gas price estimation crate to a new one built on top of alloy we lost a key optimization. The old gas price estimation updated the gas price in a background task so the total number of RPC requests was constant while they scale with the number of connected solvers and such for the new logic. For example on mainnet this causes the driver to spend a third of its RPC requests on gas price computation which is obviously insane. <img width="1187" height="304" alt="Screenshot 2026-06-29 at 09 10 08" src="https://github.com/user-attachments/assets/087a847b-6cb5-413e-ae94-681e1f1dad57" /> # Changes Implements a background task for the alloy based gas price estimator which updates the gas price once for every new block. To break up a circular dependency when creating the `Ethereum` instance in the driver I needed to move the creation of the gas price estimator into `Ethereum::new()` which in turn required a few other changes. ## How to test Added unit test Also deployed this temporarily to prod BNB and the time to convert the DTO to the domain types dropped from ~200ms to 3ms because we don't have any RPC calls on the hot path anymore. <img width="1599" height="819" alt="Screenshot 2026-06-30 at 08 26 10" src="https://github.com/user-attachments/assets/c4d00479-5826-45de-87b1-d94ff829baf1" />
1 parent f0b04eb commit 9405989

10 files changed

Lines changed: 344 additions & 160 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/autopilot/src/run.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,9 @@ pub async fn run(config: Configuration, shutdown_controller: ShutdownController)
275275
let gas_price_estimator = Arc::new(
276276
gas_price_estimation::create_priority_estimator(
277277
http_factory.create(),
278-
&web3,
278+
&web3.provider,
279279
&gas_estimators,
280+
eth.current_block(),
280281
)
281282
.await
282283
.expect("failed to create gas price estimator"),

crates/driver/src/infra/blockchain/gas.rs

Lines changed: 53 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use {
88
alloy::eips::eip1559::Eip1559Estimation,
99
anyhow::anyhow,
1010
eth_domain_types as eth,
11-
ethrpc::Web3,
11+
ethrpc::{AlloyProvider, block_stream::CurrentBlockWatcher},
1212
gas_price_estimation::{
1313
GasPriceEstimating,
1414
configurable_alloy::ConfigurableGasPriceEstimator,
@@ -17,67 +17,33 @@ use {
1717
std::sync::Arc,
1818
};
1919

20-
type MaxAdditionalTip = eth::U256;
21-
type AdditionalTipPercentage = f64;
22-
type AdditionalTip = (MaxAdditionalTip, AdditionalTipPercentage);
23-
2420
pub struct GasPriceEstimator {
2521
pub(super) gas: Arc<dyn GasPriceEstimating>,
26-
additional_tip: AdditionalTip,
27-
max_fee_per_gas: eth::U256,
28-
min_priority_fee: eth::U256,
22+
adjustments: GasPriceParameters,
2923
}
3024

3125
impl GasPriceEstimator {
3226
pub async fn new(
33-
web3: &Web3,
27+
provider: &AlloyProvider,
28+
current_block: &CurrentBlockWatcher,
3429
gas_estimator_type: &GasEstimatorType,
35-
mempools: &[mempool::Config],
30+
adjustments: GasPriceParameters,
3631
) -> Result<Self, Error> {
3732
let gas: Arc<dyn GasPriceEstimating> = match gas_estimator_type {
38-
GasEstimatorType::Web3 => Arc::new(NodeGasPriceEstimator::new(web3.provider.clone())),
33+
GasEstimatorType::Web3 => Arc::new(NodeGasPriceEstimator::new(provider.clone())),
3934
GasEstimatorType::Alloy {
4035
past_blocks,
4136
reward_percentile,
4237
} => Arc::new(ConfigurableGasPriceEstimator::new(
43-
web3.provider.clone(),
38+
provider.clone(),
4439
configs::gas_price_estimation::EstimatorConfig {
4540
past_blocks: *past_blocks,
4641
reward_percentile: *reward_percentile,
4742
},
43+
current_block.clone(),
4844
)),
4945
};
50-
// TODO: simplify logic by moving gas price adjustments out of the individual
51-
// mempool configs
52-
let additional_tip = mempools
53-
.iter()
54-
.map(|mempool| {
55-
(
56-
mempool.max_additional_tip,
57-
mempool.additional_tip_percentage,
58-
)
59-
})
60-
.next()
61-
.unwrap_or((eth::U256::ZERO, 0.));
62-
// Use the lowest max_fee_per_gas of all mempools as the max_fee_per_gas
63-
let max_fee_per_gas = mempools
64-
.iter()
65-
.map(|mempool| mempool.gas_price_cap)
66-
.min()
67-
.expect("at least one mempool");
68-
69-
// Use the highest min_priority_fee of all mempools as the min_priority_fee
70-
let min_priority_fee = mempools
71-
.iter()
72-
.map(|mempool| mempool.min_priority_fee)
73-
.max()
74-
.expect("at least one mempool");
75-
Ok(Self {
76-
gas,
77-
additional_tip,
78-
max_fee_per_gas,
79-
min_priority_fee,
80-
})
46+
Ok(Self { gas, adjustments })
8147
}
8248

8349
/// Estimates the gas price for a transaction.
@@ -88,29 +54,25 @@ impl GasPriceEstimator {
8854
let estimate = self.gas.estimate().await.map_err(Error::GasPrice)?;
8955

9056
let max_priority_fee_per_gas = {
91-
// the driver supports tweaking the tx gas price tip in case the gas
92-
// price estimator is systematically too low => compute configured tip bump
93-
let (max_additional_tip, tip_percentage_increase) = self.additional_tip;
94-
9557
// Calculate additional tip in integer space to avoid precision loss
9658
// Convert percentage to basis points (multiply by 10000) to maintain precision
97-
// e.g., tip_percentage_increase = 0.125 (12.5%) becomes 1250
59+
// e.g., additional_tip_percentage = 0.125 (12.5%) becomes 1250
9860
let overflow_err = || {
9961
Error::GasPrice(anyhow!(
10062
"overflow on multiplication (max_priority_fee_per_gas * tip_percentage_as_bps)"
10163
))
10264
};
103-
let tip_percentage_as_bps = tip_percentage_increase * 10000.0;
65+
let tip_percentage_as_bps = self.adjustments.additional_tip_factor * 10000.0;
10466
let calculated_tip = eth::U256::from(estimate.max_priority_fee_per_gas)
10567
.checked_mul(eth::U256::from(tip_percentage_as_bps))
10668
.ok_or_else(overflow_err)?
10769
/ eth::U256::from(10000u128);
10870

109-
let additional_tip = max_additional_tip.min(calculated_tip);
71+
let additional_tip = self.adjustments.max_additional_tip.min(calculated_tip);
11072

11173
// make sure we tip at least some configurable minimum amount
11274
std::cmp::max(
113-
self.min_priority_fee,
75+
self.adjustments.min_priority_fee,
11476
eth::U256::from(estimate.max_priority_fee_per_gas) + additional_tip,
11577
)
11678
};
@@ -120,7 +82,7 @@ impl GasPriceEstimator {
12082
let suggested_max_fee_per_gas = eth::U256::from(estimate.max_fee_per_gas);
12183
let suggested_max_fee_per_gas =
12284
std::cmp::max(suggested_max_fee_per_gas, max_priority_fee_per_gas);
123-
if suggested_max_fee_per_gas > self.max_fee_per_gas {
85+
if suggested_max_fee_per_gas > self.adjustments.max_fee_per_gas {
12486
return Err(Error::GasPrice(anyhow::anyhow!(
12587
"suggested gas price is higher than maximum allowed gas price (network is too \
12688
congested)"
@@ -146,3 +108,42 @@ impl GasPriceEstimating for GasPriceEstimator {
146108
self.gas.base_fee().await
147109
}
148110
}
111+
112+
/// Set of tweaks to fine tune the computed gas price.
113+
pub struct GasPriceParameters {
114+
/// Maximum priority_fee_per_gas added on top of the price suggested by the
115+
/// estimator.
116+
max_additional_tip: eth::U256,
117+
/// The max_priority_fee_per_gas suggested by the gas price estimator gets
118+
/// multiplied with this factor and get capped at `max_additional_tip`.
119+
additional_tip_factor: f64,
120+
/// Highest `max_fee_per_gas` at which we still want to submit a tx.
121+
max_fee_per_gas: eth::U256,
122+
/// We'll always tip at least this value for `max_priority_fee_per_gas`.
123+
min_priority_fee: eth::U256,
124+
}
125+
126+
pub fn adjustments(mempools: &[mempool::Config]) -> GasPriceParameters {
127+
// TODO: make configuration of those parameters more obvious by moving them
128+
// into a separate config field instead of deriving them from the mempool
129+
// configs
130+
let mut max_additional_tip = eth::U256::ZERO;
131+
let mut additional_tip_percentage = 0.0f64;
132+
let mut max_fee_per_gas = eth::U256::MAX;
133+
let mut min_priority_fee = eth::U256::ZERO;
134+
135+
for mempool in mempools {
136+
max_additional_tip = max_additional_tip.max(mempool.max_additional_tip);
137+
additional_tip_percentage =
138+
additional_tip_percentage.max(mempool.additional_tip_percentage);
139+
max_fee_per_gas = max_fee_per_gas.min(mempool.gas_price_cap);
140+
min_priority_fee = min_priority_fee.max(mempool.min_priority_fee);
141+
}
142+
143+
GasPriceParameters {
144+
max_additional_tip,
145+
additional_tip_factor: additional_tip_percentage,
146+
max_fee_per_gas,
147+
min_priority_fee,
148+
}
149+
}

crates/driver/src/infra/blockchain/mod.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
use {
2-
crate::{boundary, domain::blockchain::TxStatus},
2+
crate::{
3+
boundary,
4+
domain::blockchain::TxStatus,
5+
infra::{blockchain::gas::GasPriceParameters, config::file::GasEstimatorType},
6+
},
37
account_balances::{BalanceSimulator, SimulationError},
48
alloy::{
59
eips::eip1559::Eip1559Estimation,
@@ -104,16 +108,28 @@ impl Ethereum {
104108
pub async fn new(
105109
rpc: Rpc,
106110
addresses: contracts::Addresses,
107-
gas: Arc<GasPriceEstimator>,
108111
current_block_args: &shared::current_block::Arguments,
109112
tx_gas_limit: eth::Gas,
113+
gas_estimator_type: &GasEstimatorType,
114+
gas_adjustments: GasPriceParameters,
110115
) -> Self {
111116
let Rpc { web3, chain, args } = rpc;
112117
let current_block_stream = current_block_args
113118
.stream(args.url.clone(), web3.provider.clone())
114119
.await
115120
.expect("couldn't initialize current block stream");
116121

122+
let gas = Arc::new(
123+
GasPriceEstimator::new(
124+
&web3.provider,
125+
&current_block_stream,
126+
gas_estimator_type,
127+
gas_adjustments,
128+
)
129+
.await
130+
.expect("initialize gas price estimator"),
131+
);
132+
117133
let contracts = Contracts::new(&web3, chain, addresses)
118134
.await
119135
.expect("could not initialize important smart contracts");

crates/driver/src/run.rs

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use {
77
infra::{
88
self,
99
Api,
10-
blockchain::{self, Ethereum},
10+
blockchain::{self, Ethereum, gas},
1111
cli,
1212
config,
1313
liquidity,
@@ -16,12 +16,11 @@ use {
1616
},
1717
},
1818
clap::Parser,
19-
eth_domain_types as eth,
2019
futures::future::join_all,
2120
http_client::HttpClientFactory,
2221
shared::arguments::tracing_config,
2322
simulator::{self, Simulator},
24-
std::{net::SocketAddr, sync::Arc, time::Duration},
23+
std::{net::SocketAddr, time::Duration},
2524
tokio::sync::oneshot,
2625
};
2726

@@ -64,7 +63,16 @@ async fn run_with(args: cli::Args, addr_sender: Option<oneshot::Sender<SocketAdd
6463
tracing::info!(%commit_hash, "running driver with {config:#?}");
6564

6665
let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel();
67-
let eth = ethereum(&config, ethrpc.clone(), &args.current_block).await;
66+
let eth = Ethereum::new(
67+
ethrpc.clone(),
68+
config.contracts.clone(),
69+
&args.current_block,
70+
config.tx_gas_limit.into(),
71+
&config.gas_estimator,
72+
gas::adjustments(&config.mempools),
73+
)
74+
.await;
75+
6876
let simulator_eth = simulator::Ethereum::new(
6977
ethrpc.web3().clone(),
7078
ethrpc.chain(),
@@ -186,26 +194,6 @@ async fn ethrpc(args: &cli::Args) -> blockchain::Rpc {
186194
.expect("connect ethereum RPC")
187195
}
188196

189-
async fn ethereum(
190-
config: &infra::Config,
191-
ethrpc: blockchain::Rpc,
192-
current_block_args: &shared::current_block::Arguments,
193-
) -> Ethereum {
194-
let gas = Arc::new(
195-
blockchain::GasPriceEstimator::new(ethrpc.web3(), &config.gas_estimator, &config.mempools)
196-
.await
197-
.expect("initialize gas price estimator"),
198-
);
199-
Ethereum::new(
200-
ethrpc,
201-
config.contracts.clone(),
202-
gas,
203-
current_block_args,
204-
eth::Gas(config.tx_gas_limit),
205-
)
206-
.await
207-
}
208-
209197
async fn solvers(config: &config::Config, eth: &Ethereum) -> Vec<Solver> {
210198
join_all(
211199
config

crates/driver/src/tests/setup/solver.rs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ use {
99
competition::order,
1010
time::{self},
1111
},
12-
infra::{self, Ethereum, blockchain::contracts::Addresses, config::file::FeeHandler},
12+
infra::{
13+
self,
14+
Ethereum,
15+
blockchain::{contracts::Addresses, gas},
16+
config::file::FeeHandler,
17+
},
1318
tests::setup::blockchain::Trade,
1419
},
1520
alloy::{primitives::Address, signers::local::PrivateKeySigner},
@@ -474,17 +479,6 @@ impl Solver {
474479
})
475480
.await
476481
.unwrap();
477-
let gas = Arc::new(
478-
infra::blockchain::GasPriceEstimator::new(
479-
rpc.web3(),
480-
&Default::default(),
481-
&[infra::mempool::Config::test_config(
482-
config.blockchain.web3_url.parse().unwrap(),
483-
)],
484-
)
485-
.await
486-
.unwrap(),
487-
);
488482
let eth = Ethereum::new(
489483
rpc,
490484
Addresses {
@@ -495,12 +489,15 @@ impl Solver {
495489
cow_amm_helper_by_factory: Default::default(),
496490
flashloan_router: Some((*config.blockchain.flashloan_router.address()).into()),
497491
},
498-
gas,
499492
&shared::current_block::Arguments {
500493
block_stream_poll_interval: None,
501494
node_ws_url: Some(config.blockchain.web3_ws_url.parse().unwrap()),
502495
},
503496
eth_domain_types::Gas(eth_domain_types::U256::from(45_000_000u64)),
497+
&Default::default(),
498+
gas::adjustments(&[infra::mempool::Config::test_config(
499+
config.blockchain.web3_url.parse().unwrap(),
500+
)]),
504501
)
505502
.await;
506503

crates/gas-price-estimation/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ url = { workspace = true }
2323

2424
[dev-dependencies]
2525
mockall = { workspace = true }
26+
serde_json = { workspace = true }
2627

2728
[lints]
2829
workspace = true

0 commit comments

Comments
 (0)