Skip to content

Commit b4bb65f

Browse files
committed
Merge branch 'main' into pyth-stylus-parse-updates
2 parents 47d535a + 9dcec57 commit b4bb65f

24 files changed

+504
-355
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ apps/entropy-debugger @pyth-network/web-team
33
apps/insights @pyth-network/web-team
44
apps/staking @pyth-network/web-team
55
apps/pyth-lazer-agent @merolish
6+
apps/fortuna @tejasbadadare
67
packages/component-library @pyth-network/web-team
78
packages/known-publishers @pyth-network/web-team
89
Dockerfile.node @pyth-network/web-team

Cargo.lock

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

apps/fortuna/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
*secret*
44
*private-key*
55
.envrc
6-
fortuna.db
6+
fortuna.db*

apps/fortuna/Cargo.toml

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

66
[lib]
@@ -47,6 +47,7 @@ backoff = { version = "0.4.0", features = ["futures", "tokio"] }
4747
thiserror = "1.0.61"
4848
futures-locks = "0.7.1"
4949
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "chrono"] }
50+
num-traits = "0.2.19"
5051

5152
[dev-dependencies]
5253
axum-test = "13.1.1"

apps/fortuna/README.md

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -58,66 +58,53 @@ Fortuna supports running multiple replica instances for high availability and re
5858

5959
### Fee Management with Multiple Instances
6060

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.
61+
When running multiple Fortuna instances with different keeper wallets, the system uses a fair fee distribution strategy. Each keeper will withdraw fees from the contract to maintain a balanced distribution across all known keeper addresses and the fee manager address.
62+
63+
The fee manager (configured in the provider section) can be a separate wallet from the keeper wallets. When fees are withdrawn from the contract, they go to the fee manager wallet first, then are automatically transferred to the requesting keeper wallet.
64+
65+
**Key Configuration:**
66+
- All instances should have `keeper.private_key` and `keeper.fee_manager_private_key` provided so that each keeper can top itself up as fee manager from contract fees.
6267

6368
### Example Configurations
6469

65-
**Two Replica Setup with Fee Management:**
6670
```yaml
67-
# Replica 0 (fee manager wallet) - handles even sequence numbers + fee management
71+
# Replica 0 - handles even sequence numbers + fee management
6872
keeper:
6973
private_key:
74+
value: 0x<keeper_0_private_key>
75+
fee_manager_private_key:
7076
value: 0x<fee_manager_private_key>
77+
other_keeper_addresses:
78+
- 0x<keeper_0_address> # This replica's address
79+
- 0x<keeper_1_address> # Other replica's address
7180
replica_config:
7281
replica_id: 0
7382
total_replicas: 2
74-
backup_delay_seconds: 30
75-
run_config:
76-
disable_fee_adjustment: false # Enable fee management (default)
77-
disable_fee_withdrawal: false
83+
backup_delay_seconds: 15
84+
7885

79-
# Replica 1 (non-fee-manager wallet) - handles odd sequence numbers only
86+
# Replica 1 - handles odd sequence numbers
8087
keeper:
8188
private_key:
82-
value: 0x<other_keeper_private_key>
89+
value: 0x<keeper_1_private_key>
90+
fee_manager_private_key:
91+
value: 0x<fee_manager_private_key>
92+
other_keeper_addresses:
93+
- 0x<keeper_0_address> # Other replica's address
94+
- 0x<keeper_1_address> # This replica's address
8395
replica_config:
8496
replica_id: 1
8597
total_replicas: 2
86-
backup_delay_seconds: 30
87-
run_config:
88-
disable_fee_adjustment: true # Disable fee management
89-
disable_fee_withdrawal: true
90-
```
91-
92-
**Three Replica Setup:**
93-
```yaml
94-
# Replica 0 (fee manager wallet) - handles sequence numbers 0, 3, 6, 9, ... + fee management
95-
keeper:
96-
replica_config:
97-
replica_id: 0
98-
total_replicas: 3
99-
backup_delay_seconds: 30
100-
run_config:
101-
disable_fee_adjustment: false
102-
disable_fee_withdrawal: false
98+
backup_delay_seconds: 15
10399

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
113100
```
114101

115102
### Deployment Considerations
116103

117104
1. **Separate Wallets**: Each replica MUST use a different private key to avoid nonce conflicts
118105
2. **Fee Manager Assignment**: Set the provider's `fee_manager` address to match the primary instance's keeper wallet
119106
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)
107+
4. **Backup Delay**: Set `backup_delay_seconds` long enough to allow primary replica to process requests, but short enough for acceptable failover time (recommended: 10-30 seconds)
121108
5. **Monitoring**: Monitor each replica's processing metrics to ensure proper load distribution
122109
6. **Gas Management**: Each replica needs sufficient ETH balance for gas fees
123110

@@ -127,7 +114,6 @@ keeper:
127114
- Backup replicas wait for `backup_delay_seconds` before checking if request is still unfulfilled
128115
- If request is already fulfilled during the delay, backup replica skips processing
129116
- 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.
131117

132118
## Local Development
133119

apps/fortuna/config.sample.yaml

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ provider:
7373
# For production, you can store the private key in a file.
7474
# file: secret.txt
7575

76-
# Set this to the address of your keeper wallet if you would like the keeper wallet to
77-
# be able to withdraw fees from the contract.
78-
fee_manager: 0xADDRESS
76+
# The address of the fee manager for the provider. Only used for syncing the fee manager address to the contract.
77+
# Fee withdrawals are handled by the fee manager private key defined in the keeper config.
78+
fee_manager: 0xfee
7979
keeper:
8080
# An ethereum wallet address and private key for running the keeper service.
8181
# This does not have to be the same key as the provider's key above.
@@ -87,25 +87,24 @@ 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
90+
# Fee manager private key for fee manager operations (if not provided, fee withdrawals won't happen)
91+
fee_manager_private_key:
92+
value: 0xabcd
93+
# file: fee-manager-key.txt
94+
95+
# List of other known keeper wallet addresses for balance comparison and fair fee withdrawals.
96+
# Do not include this keeper's address.
97+
other_keeper_addresses:
98+
- 0x1234
99+
- 0x5678
96100

97101
# Multi-replica configuration
98102
# Optional: Multi-replica configuration for high availability and load distribution
99103
# Uncomment and configure for production deployments with multiple Fortuna instances
100-
# replica_config:
101-
# replica_id: 0 # Unique identifier for this replica (0, 1, 2, ...)
102-
# total_replicas: 2 # Total number of replica instances running
103-
# backup_delay_seconds: 30 # Seconds to wait before processing other replicas' requests
104-
#
105-
# Example configurations:
106-
#
107-
# Two-replica setup (Blue/Green):
108-
# - Replica 0: handles even sequence numbers (0, 2, 4, ...)
109-
# - Replica 1: handles odd sequence numbers (1, 3, 5, ...)
110-
#
104+
# See the README for more details.
105+
replica_config:
106+
replica_id: 0 # Unique identifier for this replica (0, 1, 2, ...)
107+
total_replicas: 2 # Total number of replica instances running
108+
backup_delay_seconds: 30 # Seconds to wait before processing other replicas' requests
109+
111110
# IMPORTANT: Each replica must use a different private_key to avoid nonce conflicts!

apps/fortuna/src/api/revelation.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ pub async fn revelation(
8989
.ok_or(RestError::NoPendingRequest)?;
9090
}
9191
None => {
92-
let maybe_request_fut = state.contract.get_request(state.provider_address, sequence);
92+
let maybe_request_fut = state
93+
.contract
94+
.get_request_v2(state.provider_address, sequence);
9395
let (maybe_request, current_block_number) =
9496
try_join!(maybe_request_fut, current_block_number_fut).map_err(|e| {
9597
tracing::error!(chain_id = chain_id, "RPC request failed {}", e);

apps/fortuna/src/chain/ethereum.rs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -265,29 +265,22 @@ impl<T: JsonRpcClient + 'static> PythRandom<Provider<T>> {
265265

266266
#[async_trait]
267267
impl<T: JsonRpcClient + 'static> EntropyReader for PythRandom<Provider<T>> {
268-
async fn get_request(
268+
async fn get_request_v2(
269269
&self,
270270
provider_address: Address,
271271
sequence_number: u64,
272272
) -> Result<Option<reader::Request>> {
273-
let r = self
274-
.get_request(provider_address, sequence_number)
275-
// TODO: This doesn't work for lighlink right now. Figure out how to do this in lightlink
276-
// .block(ethers::core::types::BlockNumber::Finalized)
273+
let request = self
274+
.get_request_v2(provider_address, sequence_number)
277275
.call()
278276
.await?;
279-
280-
// sequence_number == 0 means the request does not exist.
281-
if r.sequence_number != 0 {
282-
Ok(Some(reader::Request {
283-
provider: r.provider,
284-
sequence_number: r.sequence_number,
285-
block_number: r.block_number,
286-
use_blockhash: r.use_blockhash,
287-
}))
288-
} else {
289-
Ok(None)
290-
}
277+
Ok(Some(reader::Request {
278+
provider: request.provider,
279+
sequence_number: request.sequence_number,
280+
block_number: request.block_number,
281+
use_blockhash: request.use_blockhash,
282+
callback_status: reader::RequestCallbackStatus::try_from(request.callback_status)?,
283+
}))
291284
}
292285

293286
async fn get_block_number(&self, confirmed_block_status: BlockStatus) -> Result<BlockNumber> {

apps/fortuna/src/chain/reader.rs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,11 @@ pub trait EntropyReader: Send + Sync {
6060
/// Get an in-flight request (if it exists)
6161
/// Note that if we support additional blockchains in the future, the type of `provider` may
6262
/// need to become more generic.
63-
async fn get_request(&self, provider: Address, sequence_number: u64)
64-
-> Result<Option<Request>>;
63+
async fn get_request_v2(
64+
&self,
65+
provider: Address,
66+
sequence_number: u64,
67+
) -> Result<Option<Request>>;
6568

6669
async fn get_block_number(&self, confirmed_block_status: BlockStatus) -> Result<BlockNumber>;
6770

@@ -93,12 +96,48 @@ pub struct Request {
9396
// The block number where this request was created
9497
pub block_number: BlockNumber,
9598
pub use_blockhash: bool,
99+
pub callback_status: RequestCallbackStatus,
100+
}
101+
102+
/// Status values for Request.callback_status
103+
#[derive(Clone, Debug, PartialEq, Eq)]
104+
pub enum RequestCallbackStatus {
105+
/// Not a request with callback
106+
CallbackNotNecessary = 0,
107+
/// A request with callback where the callback hasn't been invoked yet
108+
CallbackNotStarted = 1,
109+
/// A request with callback where the callback is currently in flight (this state is a reentry guard)
110+
CallbackInProgress = 2,
111+
/// A request with callback where the callback has been invoked and failed
112+
CallbackFailed = 3,
113+
}
114+
115+
impl TryFrom<u8> for RequestCallbackStatus {
116+
type Error = anyhow::Error;
117+
118+
fn try_from(value: u8) -> Result<Self> {
119+
match value {
120+
0 => Ok(RequestCallbackStatus::CallbackNotNecessary),
121+
1 => Ok(RequestCallbackStatus::CallbackNotStarted),
122+
2 => Ok(RequestCallbackStatus::CallbackInProgress),
123+
3 => Ok(RequestCallbackStatus::CallbackFailed),
124+
_ => Err(anyhow::anyhow!("Invalid callback status value: {}", value)),
125+
}
126+
}
127+
}
128+
129+
impl From<RequestCallbackStatus> for u8 {
130+
fn from(status: RequestCallbackStatus) -> Self {
131+
status as u8
132+
}
96133
}
97134

98135
#[cfg(test)]
99136
pub mod mock {
100137
use {
101-
crate::chain::reader::{BlockNumber, BlockStatus, EntropyReader, Request},
138+
crate::chain::reader::{
139+
BlockNumber, BlockStatus, EntropyReader, Request, RequestCallbackStatus,
140+
},
102141
anyhow::Result,
103142
axum::async_trait,
104143
ethers::types::{Address, U256},
@@ -129,6 +168,7 @@ pub mod mock {
129168
sequence_number: s,
130169
block_number: b,
131170
use_blockhash: u,
171+
callback_status: RequestCallbackStatus::CallbackNotNecessary,
132172
})
133173
.collect(),
134174
),
@@ -148,6 +188,7 @@ pub mod mock {
148188
sequence_number: sequence,
149189
block_number,
150190
use_blockhash,
191+
callback_status: RequestCallbackStatus::CallbackNotNecessary,
151192
});
152193
self
153194
}
@@ -160,7 +201,7 @@ pub mod mock {
160201

161202
#[async_trait]
162203
impl EntropyReader for MockEntropyReader {
163-
async fn get_request(
204+
async fn get_request_v2(
164205
&self,
165206
provider: Address,
166207
sequence_number: u64,

apps/fortuna/src/command/get_request.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ pub async fn get_request(opts: &GetRequestOptions) -> Result<()> {
1414
&Config::load(&opts.config.config)?.get_chain_config(&opts.chain_id)?,
1515
)?);
1616

17-
let p = contract.get_provider_info(opts.provider).call().await?;
17+
let p = contract.get_provider_info_v2(opts.provider).call().await?;
1818

1919
tracing::info!("Found provider: {:?}", p);
2020

2121
let r = contract
22-
.get_request(opts.provider, opts.sequence)
22+
.get_request_v2(opts.provider, opts.sequence)
2323
.call()
2424
.await?;
2525
tracing::info!("Found request: {:?}", r);

0 commit comments

Comments
 (0)