Skip to content

Commit 854c72e

Browse files
authored
add support to configure max latency (#391)
* add initial code to support max latency * fix precommit * fix tests * add more tests * Refactor test_twap.rs to use random values for max_latency * Refactor price aggregation test * address comments * bump * add comment * refactor
1 parent 8207528 commit 854c72e

File tree

7 files changed

+225
-38
lines changed

7 files changed

+225
-38
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

program/c/src/oracle/oracle.h

+3-2
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,9 @@ typedef struct pc_price
195195
pc_ema_t twac_; // time-weighted average conf interval
196196
int64_t timestamp_; // unix timestamp of aggregate price
197197
uint8_t min_pub_; // min publishers for valid price
198-
int8_t drv2_; // space for future derived values
199-
int16_t drv3_; // space for future derived values
198+
int8_t message_sent_; // flag to indicate if the current aggregate price has been sent as a message to the message buffer, 0 if not sent, 1 if sent
199+
uint8_t max_latency_; // configurable max latency in slots between send and receive
200+
int8_t drv3_; // space for future derived values
200201
int32_t drv4_; // space for future derived values
201202
pc_pub_key_t prod_; // product id/ref-account
202203
pc_pub_key_t next_; // next price account in list

program/c/src/oracle/upd_aggregate.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,13 @@ static inline bool upd_aggregate( pc_price_t *ptr, uint64_t slot, int64_t timest
171171
int64_t slot_diff = ( int64_t )slot - ( int64_t )( iptr->agg_.pub_slot_ );
172172
int64_t price = iptr->agg_.price_;
173173
int64_t conf = ( int64_t )( iptr->agg_.conf_ );
174+
int64_t max_latency = ptr->max_latency_ ? ptr->max_latency_ : PC_MAX_SEND_LATENCY;
174175
if ( iptr->agg_.status_ == PC_STATUS_TRADING &&
175176
// No overflow for INT64_MIN+conf or INT64_MAX-conf as 0 < conf < INT64_MAX
176177
// These checks ensure that price - conf and price + conf do not overflow.
177178
(int64_t)0 < conf && (INT64_MIN + conf) <= price && price <= (INT64_MAX-conf) &&
178-
slot_diff >= 0 && slot_diff <= PC_MAX_SEND_LATENCY ) {
179+
// slot_diff is implicitly >= 0 due to the check in Rust code ensuring publishing_slot is always less than or equal to the current slot.
180+
slot_diff <= max_latency ) {
179181
numv += 1;
180182
prcs[ nprcs++ ] = price - conf;
181183
prcs[ nprcs++ ] = price;

program/rust/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pyth-oracle"
3-
version = "2.25.0"
3+
version = "2.26.0"
44
edition = "2021"
55
license = "Apache 2.0"
66
publish = false

program/rust/src/accounts/price.rs

+17-4
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ mod price_pythnet {
6565
/// Minimum valid publisher quotes for a succesful aggregation
6666
pub min_pub_: u8,
6767
pub message_sent_: u8,
68-
pub unused_2_: i16,
68+
/// Configurable max latency in slots between send and receive
69+
pub max_latency_: u8,
70+
/// Unused placeholder for alignment
71+
pub unused_2_: i8,
6972
pub unused_3_: i32,
7073
/// Corresponding product account
7174
pub product_account: Pubkey,
@@ -116,6 +119,7 @@ mod price_pythnet {
116119
self.agg_.price_,
117120
self.agg_.conf_,
118121
self.agg_.pub_slot_.saturating_sub(self.prev_slot_),
122+
self.max_latency_,
119123
); // pub_slot should always be >= prev_slot, but we protect ourselves against underflow just in case
120124
Ok(())
121125
} else {
@@ -172,11 +176,17 @@ mod price_pythnet {
172176
}
173177

174178
impl PriceCumulative {
175-
pub fn update(&mut self, price: i64, conf: u64, slot_gap: u64) {
179+
pub fn update(&mut self, price: i64, conf: u64, slot_gap: u64, max_latency: u8) {
176180
self.price += i128::from(price) * i128::from(slot_gap);
177181
self.conf += u128::from(conf) * u128::from(slot_gap);
182+
// Use PC_MAX_SEND_LATENCY if max_latency is 0, otherwise use max_latency
183+
let latency = if max_latency == 0 {
184+
u64::from(PC_MAX_SEND_LATENCY)
185+
} else {
186+
u64::from(max_latency)
187+
};
178188
// This is expected to saturate at 0 most of the time (while the feed is up).
179-
self.num_down_slots += slot_gap.saturating_sub(PC_MAX_SEND_LATENCY.into());
189+
self.num_down_slots += slot_gap.saturating_sub(latency);
180190
}
181191
}
182192
}
@@ -225,7 +235,10 @@ mod price_solana {
225235
/// Whether the current aggregate price has been sent as a message to the message buffer.
226236
/// 0 = false, 1 = true. (this is a u8 to make the Pod trait happy)
227237
pub message_sent_: u8,
228-
pub unused_2_: i16,
238+
/// Configurable max latency in slots between send and receive
239+
pub max_latency_: u8,
240+
/// Unused placeholder for alignment
241+
pub unused_2_: i8,
229242
pub unused_3_: i32,
230243
/// Corresponding product account
231244
pub product_account: Pubkey,

program/rust/src/tests/test_twap.rs

+99-28
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,20 @@ use {
2121

2222
#[derive(Clone, Debug, Copy)]
2323
pub struct DataEvent {
24-
price: i64,
25-
conf: u64,
26-
slot_gap: u64,
24+
price: i64,
25+
conf: u64,
26+
slot_gap: u64,
27+
max_latency: u8,
2728
}
2829

2930
impl Arbitrary for DataEvent {
3031
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
3132
DataEvent {
32-
slot_gap: u64::from(u8::arbitrary(g)) + 1, /* Slot gap is always > 1, because there
33-
* has been a succesful aggregation */
34-
price: i64::arbitrary(g),
35-
conf: u64::arbitrary(g),
33+
slot_gap: u64::from(u8::arbitrary(g)) + 1, /* Slot gap is always > 1, because there
34+
* has been a succesful aggregation */
35+
price: i64::arbitrary(g),
36+
conf: u64::arbitrary(g),
37+
max_latency: u8::arbitrary(g),
3638
}
3739
}
3840
}
@@ -44,6 +46,7 @@ impl Arbitrary for DataEvent {
4446
/// - slot_gap is a random number between 1 and u8::MAX + 1 (256)
4547
/// - price is a random i64
4648
/// - conf is a random u64
49+
/// - max_latency is a random u8
4750
#[quickcheck]
4851
fn test_twap(input: Vec<DataEvent>) -> bool {
4952
let mut price_cumulative = PriceCumulative {
@@ -56,7 +59,12 @@ fn test_twap(input: Vec<DataEvent>) -> bool {
5659
let mut data = Vec::<DataEvent>::new();
5760

5861
for data_event in input {
59-
price_cumulative.update(data_event.price, data_event.conf, data_event.slot_gap);
62+
price_cumulative.update(
63+
data_event.price,
64+
data_event.conf,
65+
data_event.slot_gap,
66+
data_event.max_latency,
67+
);
6068
data.push(data_event);
6169
price_cumulative.check_price(data.as_slice());
6270
price_cumulative.check_conf(data.as_slice());
@@ -67,7 +75,6 @@ fn test_twap(input: Vec<DataEvent>) -> bool {
6775
true
6876
}
6977

70-
7178
impl PriceCumulative {
7279
pub fn check_price(&self, data: &[DataEvent]) {
7380
assert_eq!(
@@ -87,12 +94,18 @@ impl PriceCumulative {
8794
}
8895
pub fn check_num_down_slots(&self, data: &[DataEvent]) {
8996
assert_eq!(
90-
data.iter()
91-
.fold(0, |acc, x| if x.slot_gap > PC_MAX_SEND_LATENCY.into() {
92-
acc + (x.slot_gap - PC_MAX_SEND_LATENCY as u64)
97+
data.iter().fold(0, |acc, x| {
98+
let latency_threshold = if x.max_latency == 0 {
99+
PC_MAX_SEND_LATENCY.into()
100+
} else {
101+
x.max_latency.into()
102+
};
103+
if x.slot_gap > latency_threshold {
104+
acc + (x.slot_gap - latency_threshold)
93105
} else {
94106
acc
95-
}),
107+
}
108+
}),
96109
self.num_down_slots
97110
);
98111
}
@@ -112,35 +125,65 @@ fn test_twap_unit() {
112125

113126
let data = vec![
114127
DataEvent {
115-
price: 1,
116-
conf: 2,
117-
slot_gap: 4,
128+
price: 1,
129+
conf: 2,
130+
slot_gap: 4,
131+
max_latency: 0,
132+
},
133+
DataEvent {
134+
price: i64::MAX,
135+
conf: u64::MAX,
136+
slot_gap: 1,
137+
max_latency: 0,
118138
},
119139
DataEvent {
120-
price: i64::MAX,
121-
conf: u64::MAX,
122-
slot_gap: 1,
140+
price: -10,
141+
conf: 4,
142+
slot_gap: 30,
143+
max_latency: 0,
123144
},
124145
DataEvent {
125-
price: -10,
126-
conf: 4,
127-
slot_gap: 30,
146+
price: 1,
147+
conf: 2,
148+
slot_gap: 4,
149+
max_latency: 5,
150+
},
151+
DataEvent {
152+
price: 6,
153+
conf: 7,
154+
slot_gap: 8,
155+
max_latency: 5,
128156
},
129157
];
130158

131-
price_cumulative.update(data[0].price, data[0].conf, data[0].slot_gap);
159+
price_cumulative.update(
160+
data[0].price,
161+
data[0].conf,
162+
data[0].slot_gap,
163+
data[0].max_latency,
164+
);
132165
assert_eq!(price_cumulative.price, 5);
133166
assert_eq!(price_cumulative.conf, 10);
134167
assert_eq!(price_cumulative.num_down_slots, 3);
135168
assert_eq!(price_cumulative.unused, 0);
136169

137-
price_cumulative.update(data[1].price, data[1].conf, data[1].slot_gap);
170+
price_cumulative.update(
171+
data[1].price,
172+
data[1].conf,
173+
data[1].slot_gap,
174+
data[1].max_latency,
175+
);
138176
assert_eq!(price_cumulative.price, 9_223_372_036_854_775_812i128);
139177
assert_eq!(price_cumulative.conf, 18_446_744_073_709_551_625u128);
140178
assert_eq!(price_cumulative.num_down_slots, 3);
141179
assert_eq!(price_cumulative.unused, 0);
142180

143-
price_cumulative.update(data[2].price, data[2].conf, data[2].slot_gap);
181+
price_cumulative.update(
182+
data[2].price,
183+
data[2].conf,
184+
data[2].slot_gap,
185+
data[2].max_latency,
186+
);
144187
assert_eq!(price_cumulative.price, 9_223_372_036_854_775_512i128);
145188
assert_eq!(price_cumulative.conf, 18_446_744_073_709_551_745u128);
146189
assert_eq!(price_cumulative.num_down_slots, 8);
@@ -152,7 +195,7 @@ fn test_twap_unit() {
152195
num_down_slots: 0,
153196
unused: 0,
154197
};
155-
price_cumulative_overflow.update(i64::MIN, u64::MAX, u64::MAX);
198+
price_cumulative_overflow.update(i64::MIN, u64::MAX, u64::MAX, u8::MAX);
156199
assert_eq!(
157200
price_cumulative_overflow.price,
158201
i128::MIN - i128::from(i64::MIN)
@@ -163,9 +206,38 @@ fn test_twap_unit() {
163206
);
164207
assert_eq!(
165208
price_cumulative_overflow.num_down_slots,
166-
u64::MAX - PC_MAX_SEND_LATENCY as u64
209+
u64::MAX - u64::from(u8::MAX)
167210
);
168211
assert_eq!(price_cumulative_overflow.unused, 0);
212+
213+
let mut price_cumulative_nonzero_max_latency = PriceCumulative {
214+
price: 1,
215+
conf: 2,
216+
num_down_slots: 3,
217+
unused: 0,
218+
};
219+
220+
price_cumulative_nonzero_max_latency.update(
221+
data[3].price,
222+
data[3].conf,
223+
data[3].slot_gap,
224+
data[3].max_latency,
225+
);
226+
assert_eq!(price_cumulative_nonzero_max_latency.price, 5);
227+
assert_eq!(price_cumulative_nonzero_max_latency.conf, 10);
228+
assert_eq!(price_cumulative_nonzero_max_latency.num_down_slots, 3);
229+
assert_eq!(price_cumulative_nonzero_max_latency.unused, 0);
230+
231+
price_cumulative_nonzero_max_latency.update(
232+
data[4].price,
233+
data[4].conf,
234+
data[4].slot_gap,
235+
data[4].max_latency,
236+
);
237+
assert_eq!(price_cumulative_nonzero_max_latency.price, 53);
238+
assert_eq!(price_cumulative_nonzero_max_latency.conf, 66);
239+
assert_eq!(price_cumulative_nonzero_max_latency.num_down_slots, 6);
240+
assert_eq!(price_cumulative_nonzero_max_latency.unused, 0);
169241
}
170242

171243
#[test]
@@ -224,7 +296,6 @@ fn test_twap_with_price_account() {
224296
Err(OracleError::NeedsSuccesfulAggregation)
225297
);
226298

227-
228299
assert_eq!(price_data.price_cumulative.price, 1 - 2 * 10);
229300
assert_eq!(price_data.price_cumulative.conf, 2 + 2 * 5);
230301
assert_eq!(price_data.price_cumulative.num_down_slots, 3);

0 commit comments

Comments
 (0)