Skip to content

Commit 4fae334

Browse files
feat(fortuna): add option to disable adjustment threads (#2816)
1 parent c5dacae commit 4fae334

File tree

5 files changed

+115
-45
lines changed

5 files changed

+115
-45
lines changed

apps/fortuna/README.md

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,48 +56,78 @@ Fortuna supports running multiple replica instances for high availability and re
5656
- Each replica primarily handles requests assigned to its ID
5757
- After a configurable delay, replicas will process requests from other replicas as backup (failover)
5858

59+
### Fee Management with Multiple Instances
60+
61+
When running multiple Fortuna instances with different keeper wallets but a single provider, only one instance should handle fee management. This instance needs to run using the same private key as the fee manager, because only the registerd fee manager wallet can adjust fees and withdraw funds.
62+
5963
### Example Configurations
6064

61-
**Two Replica Setup (Blue/Green):**
65+
**Two Replica Setup with Fee Management:**
6266
```yaml
63-
# Replica 0 (Blue) - handles even sequence numbers (0, 2, 4, ...)
67+
# Replica 0 (fee manager wallet) - handles even sequence numbers + fee management
6468
keeper:
69+
private_key:
70+
value: 0x<fee_manager_private_key>
6571
replica_config:
6672
replica_id: 0
6773
total_replicas: 2
6874
backup_delay_seconds: 30
75+
run_config:
76+
disable_fee_adjustment: false # Enable fee management (default)
77+
disable_fee_withdrawal: false
6978

70-
# Replica 1 (Green) - handles odd sequence numbers (1, 3, 5, ...)
79+
# Replica 1 (non-fee-manager wallet) - handles odd sequence numbers only
7180
keeper:
81+
private_key:
82+
value: 0x<other_keeper_private_key>
7283
replica_config:
7384
replica_id: 1
7485
total_replicas: 2
7586
backup_delay_seconds: 30
87+
run_config:
88+
disable_fee_adjustment: true # Disable fee management
89+
disable_fee_withdrawal: true
7690
```
7791
7892
**Three Replica Setup:**
7993
```yaml
80-
# Replica 0 - handles sequence numbers 0, 3, 6, 9, ...
94+
# Replica 0 (fee manager wallet) - handles sequence numbers 0, 3, 6, 9, ... + fee management
8195
keeper:
8296
replica_config:
8397
replica_id: 0
8498
total_replicas: 3
8599
backup_delay_seconds: 30
100+
run_config:
101+
disable_fee_adjustment: false
102+
disable_fee_withdrawal: false
103+
104+
# Replicas 1 & 2 (non-fee-manager wallets) - request processing only
105+
keeper:
106+
replica_config:
107+
replica_id: 1 # or 2
108+
total_replicas: 3
109+
backup_delay_seconds: 30
110+
run_config:
111+
disable_fee_adjustment: true
112+
disable_fee_withdrawal: true
86113
```
87114
88115
### Deployment Considerations
89116
90117
1. **Separate Wallets**: Each replica MUST use a different private key to avoid nonce conflicts
91-
2. **Backup Delay**: Set `backup_delay_seconds` long enough to allow primary replica to process requests, but short enough for acceptable failover time (recommended: 30-60 seconds)
92-
3. **Monitoring**: Monitor each replica's processing metrics to ensure proper load distribution
93-
4. **Gas Management**: Each replica needs sufficient ETH balance for gas fees
118+
2. **Fee Manager Assignment**: Set the provider's `fee_manager` address to match the primary instance's keeper wallet
119+
3. **Thread Configuration**: Only enable fee management threads on the instance using the fee manager wallet
120+
4. **Backup Delay**: Set `backup_delay_seconds` long enough to allow primary replica to process requests, but short enough for acceptable failover time (recommended: 30-60 seconds)
121+
5. **Monitoring**: Monitor each replica's processing metrics to ensure proper load distribution
122+
6. **Gas Management**: Each replica needs sufficient ETH balance for gas fees
94123

95124
### Failover Behavior
96125

97126
- Primary replica processes requests immediately
98127
- Backup replicas wait for `backup_delay_seconds` before checking if request is still unfulfilled
99128
- If request is already fulfilled during the delay, backup replica skips processing
100129
- This prevents duplicate transactions and wasted gas while ensuring reliability
130+
- Fee management operations (adjustment/withdrawal) only occur on an instance where the keeper wallet is the fee manager wallet.
101131

102132
## Local Development
103133

apps/fortuna/config.sample.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ keeper:
8787
# For production, you can store the private key in a file.
8888
# file: keeper-key.txt
8989

90+
# Runtime configuration for the keeper service
91+
# Optional: Configure which keeper threads to disable. If running multiple replicas,
92+
# only a single replica should have the fee adjustment and withdrawal threads enabled.
93+
# run_config:
94+
# disable_fee_adjustment: false # Set to true to disable automatic fee adjustment
95+
# disable_fee_withdrawal: false # Set to true to disable automatic fee withdrawal
96+
9097
# Multi-replica configuration
9198
# Optional: Multi-replica configuration for high availability and load distribution
9299
# Uncomment and configure for production deployments with multiple Fortuna instances

apps/fortuna/src/command/run.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use {
33
api::{self, ApiBlockChainState, BlockchainState, ChainId},
44
chain::ethereum::InstrumentedPythContract,
55
command::register_provider::CommitmentMetadata,
6-
config::{Commitment, Config, EthereumConfig, ProviderConfig, ReplicaConfig, RunOptions},
6+
config::{
7+
Commitment, Config, EthereumConfig, ProviderConfig, ReplicaConfig, RunConfig,
8+
RunOptions,
9+
},
710
eth_utils::traced_client::RpcMetrics,
811
history::History,
912
keeper::{self, keeper_metrics::KeeperMetrics},
@@ -101,6 +104,7 @@ pub async fn run(opts: &RunOptions) -> Result<()> {
101104
}
102105

103106
let keeper_replica_config = config.keeper.replica_config.clone();
107+
let keeper_run_config = config.keeper.run_config.clone();
104108

105109
let chains: Arc<RwLock<HashMap<ChainId, ApiBlockChainState>>> = Arc::new(RwLock::new(
106110
config
@@ -115,6 +119,7 @@ pub async fn run(opts: &RunOptions) -> Result<()> {
115119
let keeper_metrics = keeper_metrics.clone();
116120
let keeper_private_key_option = keeper_private_key_option.clone();
117121
let keeper_replica_config = keeper_replica_config.clone();
122+
let keeper_run_config = keeper_run_config.clone();
118123
let chains = chains.clone();
119124
let secret_copy = secret.clone();
120125
let rpc_metrics = rpc_metrics.clone();
@@ -129,6 +134,7 @@ pub async fn run(opts: &RunOptions) -> Result<()> {
129134
keeper_metrics.clone(),
130135
keeper_private_key_option.clone(),
131136
keeper_replica_config.clone(),
137+
keeper_run_config.clone(),
132138
chains.clone(),
133139
&secret_copy,
134140
history.clone(),
@@ -180,6 +186,7 @@ async fn setup_chain_and_run_keeper(
180186
keeper_metrics: Arc<KeeperMetrics>,
181187
keeper_private_key_option: Option<String>,
182188
keeper_replica_config: Option<ReplicaConfig>,
189+
keeper_run_config: RunConfig,
183190
chains: Arc<RwLock<HashMap<ChainId, ApiBlockChainState>>>,
184191
secret_copy: &str,
185192
history: Arc<History>,
@@ -203,6 +210,7 @@ async fn setup_chain_and_run_keeper(
203210
keeper::run_keeper_threads(
204211
keeper_private_key,
205212
keeper_replica_config,
213+
keeper_run_config,
206214
chain_config,
207215
state,
208216
keeper_metrics.clone(),

apps/fortuna/src/config.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,17 @@ fn default_chain_sample_interval() -> u64 {
350350
1
351351
}
352352

353+
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
354+
pub struct RunConfig {
355+
/// Disable automatic fee adjustment threads
356+
#[serde(default)]
357+
pub disable_fee_adjustment: bool,
358+
359+
/// Disable automatic fee withdrawal threads
360+
#[serde(default)]
361+
pub disable_fee_withdrawal: bool,
362+
}
363+
353364
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
354365
pub struct ReplicaConfig {
355366
pub replica_id: u64,
@@ -374,6 +385,10 @@ pub struct KeeperConfig {
374385

375386
#[serde(default)]
376387
pub replica_config: Option<ReplicaConfig>,
388+
389+
/// Runtime configuration for the keeper service
390+
#[serde(default)]
391+
pub run_config: RunConfig,
377392
}
378393

379394
// A secret is a string that can be provided either as a literal in the config,

apps/fortuna/src/keeper.rs

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use {
22
crate::{
33
api::{BlockchainState, ChainId},
44
chain::ethereum::{InstrumentedPythContract, InstrumentedSignablePythContract},
5-
config::{EthereumConfig, ReplicaConfig},
5+
config::{EthereumConfig, ReplicaConfig, RunConfig},
66
eth_utils::traced_client::RpcMetrics,
77
history::History,
88
keeper::{
@@ -54,10 +54,12 @@ pub enum RequestState {
5454

5555
/// Run threads to handle events for the last `BACKLOG_RANGE` blocks, watch for new blocks and
5656
/// handle any events for the new blocks.
57+
#[allow(clippy::too_many_arguments)] // Top level orchestration function that needs to configure several threads
5758
#[tracing::instrument(name = "keeper", skip_all, fields(chain_id = chain_state.id))]
5859
pub async fn run_keeper_threads(
5960
keeper_private_key: String,
6061
keeper_replica_config: Option<ReplicaConfig>,
62+
keeper_run_config: RunConfig,
6163
chain_eth_config: EthereumConfig,
6264
chain_state: BlockchainState,
6365
metrics: Arc<KeeperMetrics>,
@@ -118,44 +120,52 @@ pub async fn run_keeper_threads(
118120
);
119121

120122
// Spawn a thread that watches the keeper wallet balance and submits withdrawal transactions as needed to top-up the balance.
121-
spawn(
122-
withdraw_fees_wrapper(
123-
contract.clone(),
124-
chain_state.provider_address,
125-
WITHDRAW_INTERVAL,
126-
U256::from(chain_eth_config.min_keeper_balance),
127-
)
128-
.in_current_span(),
129-
);
123+
if !keeper_run_config.disable_fee_withdrawal {
124+
spawn(
125+
withdraw_fees_wrapper(
126+
contract.clone(),
127+
chain_state.provider_address,
128+
WITHDRAW_INTERVAL,
129+
U256::from(chain_eth_config.min_keeper_balance),
130+
)
131+
.in_current_span(),
132+
);
133+
} else {
134+
tracing::info!("Fee withdrawal thread disabled by configuration");
135+
}
130136

131137
// Spawn a thread that periodically adjusts the provider fee.
132-
spawn(
133-
adjust_fee_wrapper(
134-
contract.clone(),
135-
chain_state.clone(),
136-
chain_state.provider_address,
137-
ADJUST_FEE_INTERVAL,
138-
chain_eth_config.legacy_tx,
139-
// NOTE: we are adjusting the fees based on the maximum configured gas for user transactions.
140-
// However, the keeper will pad the gas limit for transactions (per the escalation policy) to ensure reliable submission.
141-
// Consequently, fees can be adjusted such that transactions are still unprofitable.
142-
// While we could scale up this value based on the padding, that ends up overcharging users as most transactions cost nowhere
143-
// near the maximum gas limit.
144-
// In the unlikely event that the keeper fees aren't sufficient, the solution to this is to configure the target
145-
// fee percentage to be higher on that specific chain.
146-
chain_eth_config.gas_limit,
147-
// NOTE: unwrap() here so we panic early if someone configures these values below -100.
148-
u64::try_from(100 + chain_eth_config.min_profit_pct)
149-
.expect("min_profit_pct must be >= -100"),
150-
u64::try_from(100 + chain_eth_config.target_profit_pct)
151-
.expect("target_profit_pct must be >= -100"),
152-
u64::try_from(100 + chain_eth_config.max_profit_pct)
153-
.expect("max_profit_pct must be >= -100"),
154-
chain_eth_config.fee,
155-
metrics.clone(),
156-
)
157-
.in_current_span(),
158-
);
138+
if !keeper_run_config.disable_fee_adjustment {
139+
spawn(
140+
adjust_fee_wrapper(
141+
contract.clone(),
142+
chain_state.clone(),
143+
chain_state.provider_address,
144+
ADJUST_FEE_INTERVAL,
145+
chain_eth_config.legacy_tx,
146+
// NOTE: we are adjusting the fees based on the maximum configured gas for user transactions.
147+
// However, the keeper will pad the gas limit for transactions (per the escalation policy) to ensure reliable submission.
148+
// Consequently, fees can be adjusted such that transactions are still unprofitable.
149+
// While we could scale up this value based on the padding, that ends up overcharging users as most transactions cost nowhere
150+
// near the maximum gas limit.
151+
// In the unlikely event that the keeper fees aren't sufficient, the solution to this is to configure the target
152+
// fee percentage to be higher on that specific chain.
153+
chain_eth_config.gas_limit,
154+
// NOTE: unwrap() here so we panic early if someone configures these values below -100.
155+
u64::try_from(100 + chain_eth_config.min_profit_pct)
156+
.expect("min_profit_pct must be >= -100"),
157+
u64::try_from(100 + chain_eth_config.target_profit_pct)
158+
.expect("target_profit_pct must be >= -100"),
159+
u64::try_from(100 + chain_eth_config.max_profit_pct)
160+
.expect("max_profit_pct must be >= -100"),
161+
chain_eth_config.fee,
162+
metrics.clone(),
163+
)
164+
.in_current_span(),
165+
);
166+
} else {
167+
tracing::info!("Fee adjustment thread disabled by configuration");
168+
}
159169

160170
spawn(update_commitments_loop(contract.clone(), chain_state.clone()).in_current_span());
161171

0 commit comments

Comments
 (0)