Skip to content

Commit 3eb2bea

Browse files
author
Stanisław Drozd
authored
Drozdziak1/p2w attest cont mapping reload (#330)
* pyth2wormhole-client: automatically crawl mapping based on interval * Make the mapping crawl automation seamless * pyth2wormhole-client: Make mapping crawling routine more robust This change takes care of recoverable mapping crawling errors (e.g. malformed single price on single product is no longer dropping otherwise good different prices and products in the mapping in favor of a warn message) * pyth2wormhole-client: Move mapping crawl sleep near logic it affects * pyth2wormhole-client: remove stray comment * pyth2wormhole: Fix faulty merge with master * pyth2wormhole-client: Fix mapping crawl price counting * pyth2wormhole-client: split daemon/non-daemon, improve readabi[...] ...lity and remove most warnings * pyth2wormhole-client: parts-per-thousand -> base points * pyth2wormhole-client: inaccurate comment * p2w-client: review advice - bp -> bps, std hasher -> sha256 * pyth2wormhole-client: reuse message queue across mapping lookups
1 parent d9e94b2 commit 3eb2bea

File tree

10 files changed

+575
-307
lines changed

10 files changed

+575
-307
lines changed

solana/pyth2wormhole/Cargo.lock

Lines changed: 10 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

solana/pyth2wormhole/client/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ solana-transaction-status = "=1.10.31"
3131
solitaire = {git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.8.9"}
3232
tokio = {version = "1", features = ["sync", "rt-multi-thread", "time"]}
3333
futures = "0.3.21"
34+
sha3 = "0.10.6"
35+
generic-array = "0.14.6"
3436

3537
[dev-dependencies]
3638
pyth-client = "0.5.0"

solana/pyth2wormhole/client/src/attestation_cfg.rs

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use std::{
77
str::FromStr,
88
};
99

10+
use log::info;
11+
1012
use serde::{
1113
de::Error,
1214
Deserialize,
@@ -16,8 +18,10 @@ use serde::{
1618
};
1719
use solana_program::pubkey::Pubkey;
1820

21+
use crate::BatchState;
22+
1923
/// Pyth2wormhole config specific to attestation requests
20-
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
24+
#[derive(Clone, Debug, Hash, Deserialize, Serialize, PartialEq)]
2125
pub struct AttestationConfig {
2226
#[serde(default = "default_min_msg_reuse_interval_ms")]
2327
pub min_msg_reuse_interval_ms: u64,
@@ -30,6 +34,15 @@ pub struct AttestationConfig {
3034
default // Uses Option::default() which is None
3135
)]
3236
pub mapping_addr: Option<Pubkey>,
37+
/// The known symbol list will be reloaded based off this
38+
/// interval, to account for mapping changes. Note: This interval
39+
/// will only work if the mapping address is defined. Whenever
40+
/// it's time to look up the mapping, new attestation jobs are
41+
/// started lazily, only if mapping contents affected the known
42+
/// symbol list, and before stopping the pre-existing obsolete
43+
/// jobs to maintain uninterrupted cranking.
44+
#[serde(default = "default_mapping_reload_interval_mins")]
45+
pub mapping_reload_interval_mins: u64,
3346
#[serde(default = "default_min_rpc_interval_ms")]
3447
/// Rate-limiting minimum delay between RPC requests in milliseconds"
3548
pub min_rpc_interval_ms: u64,
@@ -49,7 +62,7 @@ impl AttestationConfig {
4962
for existing_group in &self.symbol_groups {
5063
for existing_sym in &existing_group.symbols {
5164
// Check if new symbols mention this product
52-
if let Some(mut prices) = new_symbols.get_mut(&existing_sym.product_addr) {
65+
if let Some(prices) = new_symbols.get_mut(&existing_sym.product_addr) {
5366
// Prune the price if exists
5467
prices.remove(&existing_sym.price_addr);
5568
}
@@ -74,7 +87,7 @@ impl AttestationConfig {
7487
.iter_mut()
7588
.find(|g| g.group_name == group_name) // Advances the iterator and returns Some(item) on first hit
7689
{
77-
Some(mut existing_group) => existing_group.symbols.append(&mut new_symbols_vec),
90+
Some(existing_group) => existing_group.symbols.append(&mut new_symbols_vec),
7891
None if new_symbols_vec.len() != 0 => {
7992
// Group does not exist, assume defaults
8093
let new_group = SymbolGroup {
@@ -88,9 +101,30 @@ impl AttestationConfig {
88101
None => {}
89102
}
90103
}
104+
105+
pub fn as_batches(&self, max_batch_size: usize) -> Vec<BatchState> {
106+
self.symbol_groups
107+
.iter()
108+
.map(move |g| {
109+
let conditions4closure = g.conditions.clone();
110+
let name4closure = g.group_name.clone();
111+
112+
info!("Group {:?}, {} symbols", g.group_name, g.symbols.len(),);
113+
114+
// Divide group into batches
115+
g.symbols
116+
.as_slice()
117+
.chunks(max_batch_size.clone())
118+
.map(move |symbols| {
119+
BatchState::new(name4closure.clone(), symbols, conditions4closure.clone())
120+
})
121+
})
122+
.flatten()
123+
.collect()
124+
}
91125
}
92126

93-
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
127+
#[derive(Clone, Debug, Hash, Deserialize, Serialize, PartialEq)]
94128
pub struct SymbolGroup {
95129
pub group_name: String,
96130
/// Attestation conditions applied to all symbols in this group
@@ -106,6 +140,10 @@ pub const fn default_min_msg_reuse_interval_ms() -> u64 {
106140
10_000 // 10s
107141
}
108142

143+
pub const fn default_mapping_reload_interval_mins() -> u64 {
144+
15
145+
}
146+
109147
pub const fn default_min_rpc_interval_ms() -> u64 {
110148
150
111149
}
@@ -122,7 +160,7 @@ pub const fn default_max_batch_jobs() -> usize {
122160
/// of the active conditions is met. Option<> fields can be
123161
/// de-activated with None. All conditions are inactive by default,
124162
/// except for the non-Option ones.
125-
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
163+
#[derive(Clone, Debug, Hash, Deserialize, Serialize, PartialEq)]
126164
pub struct AttestationConditions {
127165
/// Baseline, unconditional attestation interval. Attestation is triggered if the specified interval elapsed since last attestation.
128166
#[serde(default = "default_min_interval_secs")]
@@ -134,9 +172,10 @@ pub struct AttestationConditions {
134172
#[serde(default = "default_max_batch_jobs")]
135173
pub max_batch_jobs: usize,
136174

137-
/// Trigger attestation if price changes by the specified percentage.
175+
/// Trigger attestation if price changes by the specified
176+
/// percentage, expressed in integer basis points (1bps = 0.01%)
138177
#[serde(default)]
139-
pub price_changed_pct: Option<f64>,
178+
pub price_changed_bps: Option<u64>,
140179

141180
/// Trigger attestation if publish_time advances at least the
142181
/// specified amount.
@@ -152,11 +191,11 @@ impl AttestationConditions {
152191
let AttestationConditions {
153192
min_interval_secs: _min_interval_secs,
154193
max_batch_jobs: _max_batch_jobs,
155-
price_changed_pct,
194+
price_changed_bps,
156195
publish_time_min_delta_secs,
157196
} = self;
158197

159-
price_changed_pct.is_some() || publish_time_min_delta_secs.is_some()
198+
price_changed_bps.is_some() || publish_time_min_delta_secs.is_some()
160199
}
161200
}
162201

@@ -165,14 +204,14 @@ impl Default for AttestationConditions {
165204
Self {
166205
min_interval_secs: default_min_interval_secs(),
167206
max_batch_jobs: default_max_batch_jobs(),
168-
price_changed_pct: None,
207+
price_changed_bps: None,
169208
publish_time_min_delta_secs: None,
170209
}
171210
}
172211
}
173212

174213
/// Config entry for a Pyth product + price pair
175-
#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq)]
214+
#[derive(Clone, Default, Debug, Hash, Deserialize, Serialize, PartialEq, Eq)]
176215
pub struct P2WSymbol {
177216
/// User-defined human-readable name
178217
pub name: Option<String>,
@@ -283,6 +322,7 @@ mod tests {
283322
max_msg_accounts: 100_000,
284323
min_rpc_interval_ms: 2123,
285324
mapping_addr: None,
325+
mapping_reload_interval_mins: 42,
286326
symbol_groups: vec![fastbois, slowbois],
287327
};
288328

@@ -302,6 +342,7 @@ mod tests {
302342
max_msg_accounts: 100,
303343
min_rpc_interval_ms: 42422,
304344
mapping_addr: None,
345+
mapping_reload_interval_mins: 42,
305346
symbol_groups: vec![],
306347
};
307348

solana/pyth2wormhole/client/src/batch_state.rs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
use futures::future::TryFutureExt;
21
use log::{
32
debug,
4-
trace,
53
warn,
64
};
75
use solana_client::nonblocking::rpc_client::RpcClient;
8-
use solana_sdk::signature::Signature;
96

107
use pyth_sdk_solana::state::PriceAccount;
118

@@ -16,31 +13,29 @@ use std::time::{
1613

1714
use crate::{
1815
AttestationConditions,
19-
ErrBox,
2016
P2WSymbol,
21-
RLMutex,
2217
};
2318

2419
/// Runtime representation of a batch. It refers to the original group
2520
/// from the config.
2621
#[derive(Debug)]
27-
pub struct BatchState<'a> {
22+
pub struct BatchState {
2823
pub group_name: String,
29-
pub symbols: &'a [P2WSymbol],
24+
pub symbols: Vec<P2WSymbol>,
3025
pub last_known_symbol_states: Vec<Option<PriceAccount>>,
3126
pub conditions: AttestationConditions,
3227
pub last_job_finished_at: Instant,
3328
}
3429

35-
impl<'a> BatchState<'a> {
30+
impl<'a> BatchState {
3631
pub fn new(
3732
group_name: String,
38-
symbols: &'a [P2WSymbol],
33+
symbols: &[P2WSymbol],
3934
conditions: AttestationConditions,
4035
) -> Self {
4136
Self {
4237
group_name,
43-
symbols,
38+
symbols: symbols.to_vec(),
4439
conditions,
4540
last_known_symbol_states: vec![None; symbols.len()],
4641
last_job_finished_at: Instant::now(),
@@ -69,7 +64,7 @@ impl<'a> BatchState<'a> {
6964

7065
// Only lookup and compare symbols if the conditions require
7166
if self.conditions.need_onchain_lookup() {
72-
let mut new_symbol_states: Vec<Option<PriceAccount>> =
67+
let new_symbol_states: Vec<Option<PriceAccount>> =
7368
match c.get_multiple_accounts(&pubkeys).await {
7469
Ok(acc_opts) => {
7570
acc_opts
@@ -120,9 +115,9 @@ impl<'a> BatchState<'a> {
120115
))
121116
}
122117

123-
// price_changed_pct
124-
} else if let Some(pct) = self.conditions.price_changed_pct {
125-
let pct = pct.abs();
118+
// price_changed_bps
119+
} else if let Some(bps) = self.conditions.price_changed_bps {
120+
let pct = bps as f64 / 100.0;
126121
let price_pct_diff = ((old.agg.price as f64 - new.agg.price as f64)
127122
/ old.agg.price as f64
128123
* 100.0)

solana/pyth2wormhole/client/src/cli.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,7 @@ pub enum Action {
123123
},
124124
#[clap(about = "Print out emitter address for the specified pyth2wormhole contract")]
125125
GetEmitter,
126-
#[clap(
127-
about = "Set the value of is_active config as ops_owner"
128-
)]
126+
#[clap(about = "Set the value of is_active config as ops_owner")]
129127
SetIsActive {
130128
/// Current ops owner keypair path
131129
#[clap(
@@ -139,5 +137,5 @@ pub enum Action {
139137
possible_values = ["true", "false"],
140138
)]
141139
new_is_active: String,
142-
}
140+
},
143141
}

0 commit comments

Comments
 (0)