-
Notifications
You must be signed in to change notification settings - Fork 331
Expand file tree
/
Copy pathconfig.rs
More file actions
319 lines (263 loc) · 11.9 KB
/
config.rs
File metadata and controls
319 lines (263 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
use {
crate::{
api::ChainId,
chain::reader::{BlockNumber, BlockStatus},
eth_utils::utils::EscalationPolicy,
},
anyhow::{anyhow, Result},
clap::{crate_authors, crate_description, crate_name, crate_version, Args, Parser},
ethers::types::Address,
std::{collections::HashMap, fs},
};
pub use {
generate::GenerateOptions, get_request::GetRequestOptions, inspect::InspectOptions,
register_provider::RegisterProviderOptions, request_randomness::RequestRandomnessOptions,
run::RunOptions, setup_provider::SetupProviderOptions, withdraw_fees::WithdrawFeesOptions,
};
mod generate;
mod get_request;
mod inspect;
mod register_provider;
mod request_randomness;
mod run;
mod setup_provider;
mod withdraw_fees;
const DEFAULT_RPC_ADDR: &str = "127.0.0.1:34000";
#[derive(Parser, Debug)]
#[command(name = crate_name!())]
#[command(author = crate_authors!())]
#[command(about = crate_description!())]
#[command(version = crate_version!())]
#[allow(clippy::large_enum_variant)]
pub enum Options {
/// Run the Randomness Service.
Run(RunOptions),
/// Register a new provider with the Pyth Random oracle.
RegisterProvider(RegisterProviderOptions),
/// Set up the provider for all the provided chains.
/// It registers, re-registers, or updates provider config on chain.
SetupProvider(SetupProviderOptions),
/// Request a random number from the contract.
RequestRandomness(RequestRandomnessOptions),
/// Inspect recent requests and find unfulfilled requests with callback.
Inspect(InspectOptions),
/// Generate a random number by running the entire protocol end-to-end
Generate(GenerateOptions),
/// Get the status of a pending request for a random number.
GetRequest(GetRequestOptions),
/// Withdraw any of the provider's accumulated fees from the contract.
WithdrawFees(WithdrawFeesOptions),
}
#[derive(Args, Clone, Debug)]
#[command(next_help_heading = "Config Options")]
#[group(id = "Config")]
pub struct ConfigOptions {
/// Path to a configuration file containing the list of supported blockchains
#[arg(long = "config")]
#[arg(env = "FORTUNA_CONFIG")]
#[arg(default_value = "config.yaml")]
pub config: String,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Config {
pub chains: HashMap<ChainId, EthereumConfig>,
pub provider: ProviderConfig,
pub keeper: KeeperConfig,
}
impl Config {
pub fn load(path: &str) -> Result<Config> {
// Open and read the YAML file
// TODO: the default serde deserialization doesn't enforce unique keys
let yaml_content = fs::read_to_string(path)?;
let config: Config = serde_yaml::from_str(&yaml_content)?;
// Run correctness checks for the config and fail if there are any issues.
for (chain_id, config) in config.chains.iter() {
if !(config.min_profit_pct <= config.target_profit_pct
&& config.target_profit_pct <= config.max_profit_pct)
{
return Err(anyhow!("chain id {:?} configuration is invalid. Config must satisfy min_profit_pct <= target_profit_pct <= max_profit_pct.", chain_id));
}
}
Ok(config)
}
pub fn get_chain_config(&self, chain_id: &ChainId) -> Result<EthereumConfig> {
self.chains.get(chain_id).cloned().ok_or(anyhow!(
"Could not find chain id {} in the configuration",
&chain_id
))
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct EthereumConfig {
/// URL of a Geth RPC endpoint to use for interacting with the blockchain.
/// TODO: Change type from String to Url
pub geth_rpc_addr: String,
/// Address of a Pyth Randomness contract to interact with.
pub contract_addr: Address,
/// reveal_delay_blocks - The difference between the block number with the
/// confirmed_block_status(see below) and the block number of a request to
/// Entropy should be greater than `reveal_delay_blocks` for Fortuna to reveal
/// its commitment.
pub reveal_delay_blocks: BlockNumber,
/// The BlockStatus of the block that is considered confirmed.
/// For example, Finalized, Safe, Latest
#[serde(default)]
pub confirmed_block_status: BlockStatus,
/// The number of blocks to look back for events that might be missed when starting the keeper
#[serde(default = "default_backlog_range")]
pub backlog_range: u64,
/// Use the legacy transaction format (for networks without EIP 1559)
#[serde(default)]
pub legacy_tx: bool,
/// The gas limit to use for entropy callback transactions.
pub gas_limit: u32,
/// The percentage multiplier to apply to priority fee estimates (100 = no change, e.g. 150 = 150% of base fee)
#[serde(default = "default_priority_fee_multiplier_pct")]
pub priority_fee_multiplier_pct: u64,
/// The escalation policy governs how the gas limit and fee are increased during backoff retries.
#[serde(default)]
pub escalation_policy: EscalationPolicyConfig,
/// The minimum percentage profit to earn as a function of the callback cost.
/// For example, 20 means a profit of 20% over the cost of a callback that uses the full gas limit.
/// The fee will be raised if the profit is less than this number.
/// The minimum value for this is -100. If set to < 0, it means the keeper may lose money on callbacks that use the full gas limit.
pub min_profit_pct: i64,
/// The target percentage profit to earn as a function of the callback cost.
/// For example, 20 means a profit of 20% over the cost of a callback that uses the full gas limit.
/// The fee will be set to this target whenever it falls outside the min/max bounds.
/// The minimum value for this is -100. If set to < 0, it means the keeper may lose money on callbacks that use the full gas limit.
pub target_profit_pct: i64,
/// The maximum percentage profit to earn as a function of the callback cost.
/// For example, 100 means a profit of 100% over the cost of a callback that uses the full gas limit.
/// The fee will be lowered if it is more profitable than specified here.
/// Must be larger than min_profit_pct.
/// The minimum value for this is -100. If set to < 0, it means the keeper may lose money on callbacks that use the full gas limit.
pub max_profit_pct: i64,
/// Minimum wallet balance for the keeper. If the balance falls below this level, the keeper will
/// withdraw fees from the contract to top up. This functionality requires the keeper to be the fee
/// manager for the provider.
#[serde(default)]
pub min_keeper_balance: u128,
/// How much the provider charges for a request on this chain.
#[serde(default)]
pub fee: u128,
/// Historical commitments made by the provider.
pub commitments: Option<Vec<Commitment>>,
/// Maximum number of hashes to record in a request.
/// This should be set according to the maximum gas limit the provider supports for callbacks.
pub max_num_hashes: Option<u32>,
/// A list of delays (in blocks) that indicates how many blocks should be delayed
/// before we process a block. For retry logic, we can process blocks multiple times
/// at each specified delay. For example: [5, 10, 20].
#[serde(default = "default_block_delays")]
pub block_delays: Vec<u64>,
}
fn default_block_delays() -> Vec<u64> {
vec![5]
}
fn default_priority_fee_multiplier_pct() -> u64 {
100
}
fn default_backlog_range() -> u64 {
1000
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct EscalationPolicyConfig {
/// The fee multiplier to apply to the fee during backoff retries.
/// The initial fee is 100% of the estimate (which itself may be padded based on our chain configuration)
/// The fee on each successive retry is multiplied by this value, with the maximum multiplier capped at `fee_multiplier_cap_pct`.
#[serde(default = "default_fee_multiplier_pct")]
pub fee_multiplier_pct: u64,
#[serde(default = "default_fee_multiplier_cap_pct")]
pub fee_multiplier_cap_pct: u64,
}
fn default_fee_multiplier_pct() -> u64 {
110
}
fn default_fee_multiplier_cap_pct() -> u64 {
200
}
impl Default for EscalationPolicyConfig {
fn default() -> Self {
Self {
fee_multiplier_pct: default_fee_multiplier_pct(),
fee_multiplier_cap_pct: default_fee_multiplier_cap_pct(),
}
}
}
impl EscalationPolicyConfig {
pub fn to_policy(&self) -> EscalationPolicy {
EscalationPolicy {
fee_multiplier_pct: self.fee_multiplier_pct,
fee_multiplier_cap_pct: self.fee_multiplier_cap_pct,
}
}
}
/// A commitment that the provider used to generate random numbers at some point in the past.
/// These historical commitments need to be stored in the configuration to support transition points where
/// the commitment changes. In theory, this information is stored on the blockchain, but unfortunately it
/// is hard to retrieve from there.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Commitment {
pub seed: [u8; 32],
pub chain_length: u64,
pub original_commitment_sequence_number: u64,
}
/// Configuration values that are common to a single provider (and shared across chains).
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct ProviderConfig {
/// The URI where clients can retrieve random values from this provider,
/// i.e., wherever fortuna for this provider will be hosted.
pub uri: String,
/// The public key of the provider whose requests the server will respond to.
pub address: Address,
/// The provider's private key, which is required to register, update the commitment,
/// or claim fees. This argument *will not* be loaded for commands that do not need
/// the private key (e.g., running the server).
pub private_key: SecretString,
/// The provider's secret which is a 64-char hex string.
/// The secret is used for generating new hash chains
pub secret: SecretString,
/// The length of the hash chain to generate.
pub chain_length: u64,
/// How frequently the hash chain is sampled -- increase this value to tradeoff more
/// compute per request for less RAM use.
#[serde(default = "default_chain_sample_interval")]
pub chain_sample_interval: u64,
/// The address of the fee manager for the provider. Set this value to the keeper wallet address to
/// enable keeper balance top-ups.
pub fee_manager: Option<Address>,
}
fn default_chain_sample_interval() -> u64 {
1
}
/// Configuration values for the keeper service that are shared across chains.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct KeeperConfig {
/// If provided, the keeper will run alongside the Fortuna API service.
/// The private key is a 20-byte (40 char) hex encoded Ethereum private key.
/// This key is required to submit transactions for entropy callback requests.
/// This key *does not need to be a registered provider*. In particular, production deployments
/// should ensure this is a different key in order to reduce the severity of security breaches.
pub private_key: SecretString,
}
// A secret is a string that can be provided either as a literal in the config,
// or in a separate file. (The separate file option is useful for 1password mounting in production.)
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct SecretString {
pub value: Option<String>,
// The name of a file containing the string to read. Note that the file contents is trimmed
// of leading/trailing whitespace when read.
pub file: Option<String>,
}
impl SecretString {
pub fn load(&self) -> Result<Option<String>> {
if let Some(v) = &self.value {
return Ok(Some(v.clone()));
}
if let Some(v) = &self.file {
return Ok(Some(fs::read_to_string(v)?.trim().to_string()));
}
Ok(None)
}
}