Skip to content

Commit f3a8f56

Browse files
authored
feat(fortuna): Dynamically increase gas limit for a transaction on failure (#2238)
* gas multiplier for tx backoff * add scaling to callback * bump version
1 parent 7cb1725 commit f3a8f56

File tree

5 files changed

+61
-7
lines changed

5 files changed

+61
-7
lines changed

apps/fortuna/Cargo.lock

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

apps/fortuna/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fortuna"
3-
version = "6.7.2"
3+
version = "6.8.0"
44
edition = "2021"
55

66
[dependencies]

apps/fortuna/config.sample.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ chains:
66
# Keeper configuration for the chain
77
reveal_delay_blocks: 0
88
gas_limit: 500000
9+
# Increase the transaction gas limit by 10% each time the callback fails
10+
# defaults to 100 (i.e., don't change the gas limit) if not specified.
11+
backoff_gas_multiplier_pct: 110
912
min_keeper_balance: 100000000000000000
1013

1114
# Provider configuration

apps/fortuna/src/config.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ pub struct EthereumConfig {
134134
/// The gas limit to use for entropy callback transactions.
135135
pub gas_limit: u64,
136136

137+
/// The percentage multiplier to apply to the gas limit for each backoff.
138+
#[serde(default = "default_backoff_gas_multiplier_pct")]
139+
pub backoff_gas_multiplier_pct: u64,
140+
137141
/// The minimum percentage profit to earn as a function of the callback cost.
138142
/// For example, 20 means a profit of 20% over the cost of the callback.
139143
/// The fee will be raised if the profit is less than this number.
@@ -172,6 +176,10 @@ pub struct EthereumConfig {
172176
pub priority_fee_multiplier_pct: u64,
173177
}
174178

179+
fn default_backoff_gas_multiplier_pct() -> u64 {
180+
100
181+
}
182+
175183
/// A commitment that the provider used to generate random numbers at some point in the past.
176184
/// These historical commitments need to be stored in the configuration to support transition points where
177185
/// the commitment changes. In theory, this information is stored on the blockchain, but unfortunately it

apps/fortuna/src/keeper.rs

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ const UPDATE_COMMITMENTS_INTERVAL: Duration = Duration::from_secs(30);
5555
const UPDATE_COMMITMENTS_THRESHOLD_FACTOR: f64 = 0.95;
5656
/// Rety last N blocks
5757
const RETRY_PREVIOUS_BLOCKS: u64 = 100;
58+
/// By default, we scale the gas estimate by 25% when submitting the tx.
59+
const DEFAULT_GAS_ESTIMATE_MULTIPLIER_PCT: u64 = 125;
5860

5961
#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
6062
pub struct AccountLabel {
@@ -254,6 +256,7 @@ pub async fn run_keeper_threads(
254256
},
255257
contract.clone(),
256258
gas_limit,
259+
chain_eth_config.backoff_gas_multiplier_pct,
257260
chain_state.clone(),
258261
metrics.clone(),
259262
fulfilled_requests_cache.clone(),
@@ -279,6 +282,7 @@ pub async fn run_keeper_threads(
279282
rx,
280283
Arc::clone(&contract),
281284
gas_limit,
285+
chain_eth_config.backoff_gas_multiplier_pct,
282286
metrics.clone(),
283287
fulfilled_requests_cache.clone(),
284288
)
@@ -303,7 +307,9 @@ pub async fn run_keeper_threads(
303307
chain_state.provider_address,
304308
ADJUST_FEE_INTERVAL,
305309
chain_eth_config.legacy_tx,
306-
chain_eth_config.gas_limit,
310+
// NOTE: we adjust fees based on the maximum gas that the keeper will submit a callback with.
311+
// This number is *larger* than the configured gas limit, as we pad gas on transaction submission for reliability.
312+
(chain_eth_config.gas_limit * DEFAULT_GAS_ESTIMATE_MULTIPLIER_PCT) / 100,
307313
chain_eth_config.min_profit_pct,
308314
chain_eth_config.target_profit_pct,
309315
chain_eth_config.max_profit_pct,
@@ -372,6 +378,7 @@ pub async fn process_event_with_backoff(
372378
chain_state: BlockchainState,
373379
contract: Arc<InstrumentedSignablePythContract>,
374380
gas_limit: U256,
381+
backoff_gas_multiplier_pct: u64,
375382
metrics: Arc<KeeperMetrics>,
376383
) {
377384
let start_time = std::time::Instant::now();
@@ -388,13 +395,35 @@ pub async fn process_event_with_backoff(
388395
max_elapsed_time: Some(Duration::from_secs(300)), // retry for 5 minutes
389396
..Default::default()
390397
};
398+
399+
let current_multiplier = Arc::new(AtomicU64::new(DEFAULT_GAS_ESTIMATE_MULTIPLIER_PCT));
400+
391401
match backoff::future::retry_notify(
392402
backoff,
393403
|| async {
394-
process_event(&event, &chain_state, &contract, gas_limit, metrics.clone()).await
404+
let multiplier = current_multiplier.load(std::sync::atomic::Ordering::Relaxed);
405+
process_event(
406+
&event,
407+
&chain_state,
408+
&contract,
409+
gas_limit,
410+
multiplier,
411+
metrics.clone(),
412+
)
413+
.await
395414
},
396415
|e, dur| {
397-
tracing::error!("Error happened at {:?}: {}", dur, e);
416+
let multiplier = current_multiplier.load(std::sync::atomic::Ordering::Relaxed);
417+
tracing::error!(
418+
"Error at duration {:?} with gas multiplier {}: {}",
419+
dur,
420+
multiplier,
421+
e
422+
);
423+
current_multiplier.store(
424+
multiplier.saturating_mul(backoff_gas_multiplier_pct) / 100,
425+
std::sync::atomic::Ordering::Relaxed,
426+
);
398427
},
399428
)
400429
.await
@@ -436,6 +465,8 @@ pub async fn process_event(
436465
chain_config: &BlockchainState,
437466
contract: &InstrumentedSignablePythContract,
438467
gas_limit: U256,
468+
// A value of 100 submits the tx with the same gas as the estimate.
469+
gas_estimate_multiplier_pct: u64,
439470
metrics: Arc<KeeperMetrics>,
440471
) -> Result<(), backoff::Error<anyhow::Error>> {
441472
// ignore requests that are not for the configured provider
@@ -466,6 +497,8 @@ pub async fn process_event(
466497
backoff::Error::transient(anyhow!("Error estimating gas for reveal: {:?}", e))
467498
})?;
468499

500+
// The gas limit on the simulated transaction is the configured gas limit on the chain,
501+
// but we are willing to pad the gas a bit to ensure reliable submission.
469502
if gas_estimate > gas_limit {
470503
return Err(backoff::Error::permanent(anyhow!(
471504
"Gas estimate for reveal with callback is higher than the gas limit {} > {}",
@@ -474,8 +507,10 @@ pub async fn process_event(
474507
)));
475508
}
476509

477-
// Pad the gas estimate by 25% after checking it against the gas limit
478-
let gas_estimate = gas_estimate.saturating_mul(5.into()) / 4;
510+
// Pad the gas estimate after checking it against the simulation gas limit, ensuring that
511+
// the padded gas estimate doesn't exceed the maximum amount of gas we are willing to use.
512+
let gas_estimate = gas_estimate.saturating_mul(gas_estimate_multiplier_pct.into()) / 100;
513+
let gas_estimate = gas_estimate.min((gas_limit * DEFAULT_GAS_ESTIMATE_MULTIPLIER_PCT) / 100);
479514

480515
let contract_call = contract
481516
.reveal_with_callback(
@@ -589,6 +624,7 @@ pub async fn process_block_range(
589624
block_range: BlockRange,
590625
contract: Arc<InstrumentedSignablePythContract>,
591626
gas_limit: U256,
627+
backoff_gas_multiplier_pct: u64,
592628
chain_state: api::BlockchainState,
593629
metrics: Arc<KeeperMetrics>,
594630
fulfilled_requests_cache: Arc<RwLock<HashSet<u64>>>,
@@ -612,6 +648,7 @@ pub async fn process_block_range(
612648
},
613649
contract.clone(),
614650
gas_limit,
651+
backoff_gas_multiplier_pct,
615652
chain_state.clone(),
616653
metrics.clone(),
617654
fulfilled_requests_cache.clone(),
@@ -634,6 +671,7 @@ pub async fn process_single_block_batch(
634671
block_range: BlockRange,
635672
contract: Arc<InstrumentedSignablePythContract>,
636673
gas_limit: U256,
674+
backoff_gas_multiplier_pct: u64,
637675
chain_state: api::BlockchainState,
638676
metrics: Arc<KeeperMetrics>,
639677
fulfilled_requests_cache: Arc<RwLock<HashSet<u64>>>,
@@ -660,6 +698,7 @@ pub async fn process_single_block_batch(
660698
chain_state.clone(),
661699
contract.clone(),
662700
gas_limit,
701+
backoff_gas_multiplier_pct,
663702
metrics.clone(),
664703
)
665704
.in_current_span(),
@@ -806,6 +845,7 @@ pub async fn process_new_blocks(
806845
mut rx: mpsc::Receiver<BlockRange>,
807846
contract: Arc<InstrumentedSignablePythContract>,
808847
gas_limit: U256,
848+
backoff_gas_multiplier_pct: u64,
809849
metrics: Arc<KeeperMetrics>,
810850
fulfilled_requests_cache: Arc<RwLock<HashSet<u64>>>,
811851
) {
@@ -816,6 +856,7 @@ pub async fn process_new_blocks(
816856
block_range,
817857
Arc::clone(&contract),
818858
gas_limit,
859+
backoff_gas_multiplier_pct,
819860
chain_state.clone(),
820861
metrics.clone(),
821862
fulfilled_requests_cache.clone(),
@@ -832,6 +873,7 @@ pub async fn process_backlog(
832873
backlog_range: BlockRange,
833874
contract: Arc<InstrumentedSignablePythContract>,
834875
gas_limit: U256,
876+
backoff_gas_multiplier_pct: u64,
835877
chain_state: BlockchainState,
836878
metrics: Arc<KeeperMetrics>,
837879
fulfilled_requests_cache: Arc<RwLock<HashSet<u64>>>,
@@ -841,6 +883,7 @@ pub async fn process_backlog(
841883
backlog_range,
842884
contract,
843885
gas_limit,
886+
backoff_gas_multiplier_pct,
844887
chain_state,
845888
metrics,
846889
fulfilled_requests_cache,

0 commit comments

Comments
 (0)