Skip to content

Commit 1895c05

Browse files
committed
gr
1 parent b8b7b87 commit 1895c05

File tree

4 files changed

+104
-51
lines changed

4 files changed

+104
-51
lines changed

apps/fortuna/src/api.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,6 @@ pub struct BlockchainState {
9292
/// The BlockStatus of the block that is considered to be confirmed on the blockchain.
9393
/// For eg., Finalized, Safe
9494
pub confirmed_block_status: BlockStatus,
95-
96-
/// The maximum percentage that the gas limit can be multiplied by during backoff retries
97-
pub backoff_gas_multiplier_cap_pct: u64,
9895
}
9996

10097
pub enum RestError {
@@ -215,7 +212,6 @@ mod test {
215212
provider_address: PROVIDER,
216213
reveal_delay_blocks: 1,
217214
confirmed_block_status: BlockStatus::Latest,
218-
backoff_gas_multiplier_cap_pct: 500,
219215
};
220216

221217
let metrics_registry = Arc::new(RwLock::new(Registry::default()));
@@ -228,8 +224,7 @@ mod test {
228224
contract: avax_read.clone(),
229225
provider_address: PROVIDER,
230226
reveal_delay_blocks: 2,
231-
confirmed_block_status: BlockStatus::Latest,
232-
backoff_gas_multiplier_cap_pct: 500,
227+
confirmed_block_status: BlockStatus::Latest
233228
};
234229

235230
let mut chains = HashMap::new();

apps/fortuna/src/command/run.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,7 @@ async fn setup_chain_state(
295295
contract,
296296
provider_address: *provider,
297297
reveal_delay_blocks: chain_config.reveal_delay_blocks,
298-
confirmed_block_status: chain_config.confirmed_block_status,
299-
backoff_gas_multiplier_cap_pct: chain_config.backoff_gas_multiplier_cap_pct,
298+
confirmed_block_status: chain_config.confirmed_block_status
300299
};
301300
Ok(state)
302301
}

apps/fortuna/src/config.rs

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,8 @@ 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,
137+
/// The escalation policy governs how the gas limit and fee are increased during backoff retries.
138+
pub escalation_policy: EscalationPolicyConfig,
140139

141140
/// The minimum percentage profit to earn as a function of the callback cost.
142141
/// For example, 20 means a profit of 20% over the cost of the callback.
@@ -170,25 +169,79 @@ pub struct EthereumConfig {
170169
/// Maximum number of hashes to record in a request.
171170
/// This should be set according to the maximum gas limit the provider supports for callbacks.
172171
pub max_num_hashes: Option<u32>,
172+
}
173+
174+
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
175+
pub struct EscalationPolicyConfig {
176+
/// The initial gas multiplier to apply to the gas limit.
177+
#[serde(default = "default_initial_gas_multiplier_pct")]
178+
pub initial_gas_multiplier_pct: u64,
179+
180+
/// The gas multiplier to apply to the gas limit during backoff retries.
181+
/// The gas on each successive retry is multiplied by this value, with the maximum multiplier capped at `gas_multiplier_cap_pct`.
182+
#[serde(default = "default_gas_multiplier_pct")]
183+
pub gas_multiplier_pct: u64,
184+
/// The maximum gas multiplier to apply to the gas limit during backoff retries.
185+
#[serde(default = "default_gas_multiplier_cap_pct")]
186+
pub gas_multiplier_cap_pct: u64,
187+
188+
/// The initial fee multiplier to apply to the fee.
189+
#[serde(default = "default_initial_fee_multiplier_pct")]
190+
pub initial_fee_multiplier_pct: u64,
191+
192+
/// The fee multiplier to apply to the fee during backoff retries.
193+
/// The fee on each successive retry is multiplied by this value, with the maximum multiplier capped at `fee_multiplier_cap_pct`.
194+
#[serde(default = "default_fee_multiplier_pct")]
195+
pub fee_multiplier_pct: u64,
196+
#[serde(default = "default_fee_multiplier_cap_pct")]
197+
pub fee_multiplier_cap_pct: u64,
198+
}
199+
200+
impl EscalationPolicyConfig {
201+
pub fn get_gas_multiplier_pct(&self, num_retries: u64) -> u64 {
202+
self.apply_escalation_policy(num_retries, self.initial_gas_multiplier_pct, self.gas_multiplier_pct, self.gas_multiplier_cap_pct)
203+
}
173204

174-
/// The percentage multiplier to apply to the priority fee (100 = no change, e.g. 150 = 150% of base fee)
175-
#[serde(default = "default_priority_fee_multiplier_pct")]
176-
pub priority_fee_multiplier_pct: u64,
205+
pub fn get_fee_multiplier_pct(&self, num_retries: u64) -> u64 {
206+
self.apply_escalation_policy(num_retries, self.initial_fee_multiplier_pct, self.fee_multiplier_pct, self.fee_multiplier_cap_pct)
207+
}
177208

178-
/// The maximum percentage that the gas limit can be multiplied by during backoff retries
179-
/// For example, 500 means the gas limit can be increased up to 500% of the original estimate
180-
#[serde(default = "default_backoff_gas_multiplier_cap_pct")]
181-
pub backoff_gas_multiplier_cap_pct: u64,
209+
fn apply_escalation_policy(&self, num_retries: u64, initial: u64, multiplier: u64, cap: u64) -> u64 {
210+
let mut current = initial;
211+
let mut i = 0;
212+
while i < num_retries && current < cap {
213+
current = current.saturating_mul(multiplier) / 100;
214+
i += 1;
215+
}
216+
217+
current.min(cap)
218+
}
182219
}
183220

184-
fn default_backoff_gas_multiplier_cap_pct() -> u64 {
185-
500 // Default to 500% (5x) maximum gas multiplier
221+
fn default_initial_gas_multiplier_pct() -> u64 {
222+
125
186223
}
187224

188-
fn default_backoff_gas_multiplier_pct() -> u64 {
225+
fn default_gas_multiplier_pct() -> u64 {
189226
100
190227
}
191228

229+
fn default_gas_multiplier_cap_pct() -> u64 {
230+
500
231+
}
232+
233+
fn default_initial_fee_multiplier_pct() -> u64 {
234+
100
235+
}
236+
237+
fn default_fee_multiplier_pct() -> u64 {
238+
110
239+
}
240+
241+
fn default_fee_multiplier_cap_pct() -> u64 {
242+
200
243+
}
244+
192245
/// A commitment that the provider used to generate random numbers at some point in the past.
193246
/// These historical commitments need to be stored in the configuration to support transition points where
194247
/// the commitment changes. In theory, this information is stored on the blockchain, but unfortunately it

apps/fortuna/src/keeper.rs

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use {
99
traced_client::{RpcMetrics, TracedClient},
1010
},
1111
config::EthereumConfig,
12+
config::EscalationPolicyConfig,
1213
},
1314
anyhow::{anyhow, Result},
1415
backoff::ExponentialBackoff,
@@ -248,10 +249,6 @@ pub async fn run_keeper_threads(
248249
let latest_safe_block = get_latest_safe_block(&chain_state).in_current_span().await;
249250
tracing::info!("latest safe block: {}", &latest_safe_block);
250251

251-
// Update BlockchainState with the gas multiplier cap from config
252-
let mut chain_state = chain_state;
253-
chain_state.backoff_gas_multiplier_cap_pct = chain_eth_config.backoff_gas_multiplier_cap_pct;
254-
255252
let contract = Arc::new(
256253
InstrumentedSignablePythContract::from_config(
257254
&chain_eth_config,
@@ -276,7 +273,7 @@ pub async fn run_keeper_threads(
276273
},
277274
contract.clone(),
278275
gas_limit,
279-
chain_eth_config.backoff_gas_multiplier_pct,
276+
chain_eth_config.escalation_policy,
280277
chain_state.clone(),
281278
metrics.clone(),
282279
fulfilled_requests_cache.clone(),
@@ -302,7 +299,7 @@ pub async fn run_keeper_threads(
302299
rx,
303300
Arc::clone(&contract),
304301
gas_limit,
305-
chain_eth_config.backoff_gas_multiplier_pct,
302+
chain_eth_config.escalation_policy,
306303
metrics.clone(),
307304
fulfilled_requests_cache.clone(),
308305
)
@@ -327,9 +324,14 @@ pub async fn run_keeper_threads(
327324
chain_state.provider_address,
328325
ADJUST_FEE_INTERVAL,
329326
chain_eth_config.legacy_tx,
330-
// NOTE: we adjust fees based on the maximum gas that the keeper will submit a callback with.
331-
// This number is *larger* than the configured gas limit, as we pad gas on transaction submission for reliability.
332-
(chain_eth_config.gas_limit * DEFAULT_GAS_ESTIMATE_MULTIPLIER_PCT) / 100,
327+
// NOTE: we are adjusting the fees based on the maximum configured gas for user transactions.
328+
// However, the keeper will pad the gas limit for transactions (per the escalation policy) to ensure reliable submission.
329+
// Consequently, fees can be adjusted such that transactions are still unprofitable.
330+
// While we could scale up this value based on the padding, that ends up overcharging users as most transactions cost nowhere
331+
// near the maximum gas limit.
332+
// In the unlikely event that the keeper fees aren't sufficient, the solution to this is to configure the target
333+
// fee percentage to be higher on that specific chain.
334+
chain_eth_config.gas_limit,
333335
chain_eth_config.min_profit_pct,
334336
chain_eth_config.target_profit_pct,
335337
chain_eth_config.max_profit_pct,
@@ -398,7 +400,7 @@ pub async fn process_event_with_backoff(
398400
chain_state: BlockchainState,
399401
contract: Arc<InstrumentedSignablePythContract>,
400402
gas_limit: U256,
401-
backoff_gas_multiplier_pct: u64,
403+
escalation_policy: EscalationPolicyConfig,
402404
metrics: Arc<KeeperMetrics>,
403405
) {
404406
let start_time = std::time::Instant::now();
@@ -414,36 +416,35 @@ pub async fn process_event_with_backoff(
414416
..Default::default()
415417
};
416418

417-
let current_multiplier = Arc::new(AtomicU64::new(DEFAULT_GAS_ESTIMATE_MULTIPLIER_PCT));
419+
let num_retries = Arc::new(AtomicU64::new(0));
418420

419421
let success = backoff::future::retry_notify(
420422
backoff,
421423
|| async {
422-
let multiplier = current_multiplier.load(std::sync::atomic::Ordering::Relaxed);
424+
let num_retries = num_retries.load(std::sync::atomic::Ordering::Relaxed);
425+
426+
let gas_multiplier_pct = escalation_policy.get_gas_multiplier_pct(num_retries);
427+
let fee_multiplier_pct = escalation_policy.get_fee_multiplier_pct(num_retries);
423428
process_event(
424429
&event,
425430
&chain_state,
426431
&contract,
427432
gas_limit,
428-
multiplier,
433+
gas_multiplier_pct,
434+
fee_multiplier_pct,
429435
metrics.clone(),
430436
)
431437
.await
432438
},
433439
|e, dur| {
434-
let multiplier = current_multiplier.load(std::sync::atomic::Ordering::Relaxed);
440+
let retry_number = num_retries.load(std::sync::atomic::Ordering::Relaxed);
435441
tracing::error!(
436-
"Error at duration {:?} with gas multiplier {}: {}",
442+
"Error on retry {} at duration {:?}: {}",
443+
retry_number,
437444
dur,
438-
multiplier,
439445
e
440446
);
441-
// Calculate new multiplier with backoff, capped by backoff_gas_multiplier_cap_pct
442-
let new_multiplier = multiplier.saturating_mul(backoff_gas_multiplier_pct) / 100;
443-
current_multiplier.store(
444-
std::cmp::min(new_multiplier, chain_state.backoff_gas_multiplier_cap_pct),
445-
std::sync::atomic::Ordering::Relaxed,
446-
);
447+
num_retries.store(retry_number + 1, std::sync::atomic::Ordering::Relaxed);
447448
},
448449
)
449450
.await;
@@ -501,8 +502,9 @@ pub async fn process_event(
501502
chain_config: &BlockchainState,
502503
contract: &InstrumentedSignablePythContract,
503504
gas_limit: U256,
504-
// A value of 100 submits the tx with the same gas as the estimate.
505+
// A value of 100 submits the tx with the same gas/fee as the estimate.
505506
gas_estimate_multiplier_pct: u64,
507+
fee_estimate_multiplier_pct: u64,
506508
metrics: Arc<KeeperMetrics>,
507509
) -> Result<(), backoff::Error<anyhow::Error>> {
508510
// ignore requests that are not for the configured provider
@@ -546,9 +548,7 @@ pub async fn process_event(
546548
// Pad the gas estimate after checking it against the simulation gas limit, ensuring that
547549
// the padded gas estimate doesn't exceed the maximum amount of gas we are willing to use.
548550
let gas_estimate = gas_estimate.saturating_mul(gas_estimate_multiplier_pct.into()) / 100;
549-
// Apply the configurable cap from backoff_gas_multiplier_cap_pct
550-
let max_gas_estimate = gas_limit.saturating_mul(chain_config.backoff_gas_multiplier_cap_pct.into()) / 100;
551-
let gas_estimate = gas_estimate.min(max_gas_estimate);
551+
552552

553553
let contract_call = contract
554554
.reveal_with_callback(
@@ -561,13 +561,19 @@ pub async fn process_event(
561561

562562
let client = contract.client();
563563
let mut transaction = contract_call.tx.clone();
564+
564565
// manually fill the tx with the gas info, so we can log the details in case of error
565566
client
566567
.fill_transaction(&mut transaction, None)
567568
.await
568569
.map_err(|e| {
569570
backoff::Error::transient(anyhow!("Error filling the reveal transaction: {:?}", e))
570571
})?;
572+
573+
// Apply the fee escalation policy. Note: the unwrap_or_default should never default as we have a gas oracle
574+
// in the client that sets the gas price.
575+
transaction.set_gas_price(transaction.gas_price().unwrap_or_default().saturating_mul(fee_estimate_multiplier_pct.into()) / 100);
576+
571577
let pending_tx = client
572578
.send_transaction(transaction.clone(), None)
573579
.await
@@ -883,7 +889,7 @@ pub async fn process_new_blocks(
883889
mut rx: mpsc::Receiver<BlockRange>,
884890
contract: Arc<InstrumentedSignablePythContract>,
885891
gas_limit: U256,
886-
backoff_gas_multiplier_pct: u64,
892+
escalation_policy: EscalationPolicyConfig,
887893
metrics: Arc<KeeperMetrics>,
888894
fulfilled_requests_cache: Arc<RwLock<HashSet<u64>>>,
889895
) {
@@ -894,7 +900,7 @@ pub async fn process_new_blocks(
894900
block_range,
895901
Arc::clone(&contract),
896902
gas_limit,
897-
backoff_gas_multiplier_pct,
903+
escalation_policy,
898904
chain_state.clone(),
899905
metrics.clone(),
900906
fulfilled_requests_cache.clone(),
@@ -911,7 +917,7 @@ pub async fn process_backlog(
911917
backlog_range: BlockRange,
912918
contract: Arc<InstrumentedSignablePythContract>,
913919
gas_limit: U256,
914-
backoff_gas_multiplier_pct: u64,
920+
escalation_policy: EscalationPolicyConfig,
915921
chain_state: BlockchainState,
916922
metrics: Arc<KeeperMetrics>,
917923
fulfilled_requests_cache: Arc<RwLock<HashSet<u64>>>,
@@ -921,7 +927,7 @@ pub async fn process_backlog(
921927
backlog_range,
922928
contract,
923929
gas_limit,
924-
backoff_gas_multiplier_pct,
930+
escalation_policy,
925931
chain_state,
926932
metrics,
927933
fulfilled_requests_cache,

0 commit comments

Comments
 (0)