Skip to content

Commit 65949e0

Browse files
authored
feat: aggregate in the same slot (#394)
* update aggregation logic to aggregate in the same slot * Add features.h to .gitignore * Refactor test_upd_aggregate.rs: Clean up imports and formatting, update test cases * add prev_twap_, prev_twac_ and prev_price_cumulative * remove twap and twac from test_up_aggregate * fix test_sizes * fix borrow reference bug * fix build * fix logic * fix test_publish * format * fix test_publish_batch * fix test_upd_price_no_fail_on_error * fix test_upd_price_v2 * fix logic * refactor * update function desc * fix logic * add comments * fix tests * add ema test * reduce PC_NUM_COMP_PYTHNET to 64 * fix tests * add comments * revert to use PriceComponentArrayWrapper * refactor * update c format * revert format * revert format * gitignore .clang-format * remove clang-format * revert format * revert format * format * revert format * revert format * fix comment * remove comment * add comment * refactor * add guard for first price update after deployment * add back deleted test in test_publish * add tests for prev values to test_upd_price * update comment * add test * use last_slot_ instead of agg_.pub_slot_ * add more asserts * remove crank again asserts * fix * address comments * address comments * remove print statement * add test to simulate program upgrade * refactor * address comments * address comments * address comments * address comments * merge test_upd_price with test_upd_price_v2 * add asserts
1 parent 14fbdef commit 65949e0

16 files changed

+701
-667
lines changed

.gitignore

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ target
55
cmake-build-*
66
/venv
77

8+
# CMake files
9+
features.h
10+
811
# IntelliJ / CLion configuration
912
.idea
1013
*.iml
1114

12-
# CMake files
13-
features.h
15+
# Clang format
16+
.clang-format

program/c/src/oracle/oracle.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ extern "C" {
2323
#define PC_MAP_TABLE_SIZE 640
2424

2525
// Total price component slots available
26-
#define PC_NUM_COMP_PYTHNET 128
26+
#define PC_NUM_COMP_PYTHNET 127
2727

2828
// PC_NUM_COMP - number of price components in use
2929
// Not whole PC_NUM_COMP_PYTHNET because of stack issues appearing in upd_aggregate()

program/c/src/oracle/upd_aggregate.h

-8
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,6 @@ static inline void upd_twap(
134134
// update aggregate price
135135
static inline bool upd_aggregate( pc_price_t *ptr, uint64_t slot, int64_t timestamp )
136136
{
137-
// Update the value of the previous price, if it had TRADING status.
138-
if ( ptr->agg_.status_ == PC_STATUS_TRADING ) {
139-
ptr->prev_slot_ = ptr->agg_.pub_slot_;
140-
ptr->prev_price_ = ptr->agg_.price_;
141-
ptr->prev_conf_ = ptr->agg_.conf_;
142-
ptr->prev_timestamp_ = ptr->timestamp_;
143-
}
144-
145137
// update aggregate details ready for next slot
146138
ptr->valid_slot_ = ptr->agg_.pub_slot_;// valid slot-time of agg. price
147139
ptr->agg_.pub_slot_ = slot; // publish slot-time of agg. price

program/rust/src/accounts/price.rs

+75-24
Original file line numberDiff line numberDiff line change
@@ -31,62 +31,113 @@ mod price_pythnet {
3131
},
3232
error::OracleError,
3333
},
34+
std::ops::{
35+
Deref,
36+
DerefMut,
37+
Index,
38+
IndexMut,
39+
},
3440
};
3541

42+
#[repr(C)]
43+
#[derive(Copy, Clone)]
44+
pub struct PriceComponentArrayWrapper([PriceComponent; PC_NUM_COMP_PYTHNET as usize]);
45+
46+
// Implementing Index and IndexMut allows PriceComponentArrayWrapper to use array indexing directly,
47+
// such as price_account.comp_[i], making it behave more like a native array or slice.
48+
impl Index<usize> for PriceComponentArrayWrapper {
49+
type Output = PriceComponent;
50+
51+
fn index(&self, index: usize) -> &Self::Output {
52+
&self.0[index]
53+
}
54+
}
55+
impl IndexMut<usize> for PriceComponentArrayWrapper {
56+
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
57+
&mut self.0[index]
58+
}
59+
}
60+
61+
// Implementing Deref and DerefMut allows PriceComponentArrayWrapper to use slice methods directly,
62+
// such as len(), making it behave more like a native array or slice.
63+
impl Deref for PriceComponentArrayWrapper {
64+
type Target = [PriceComponent];
65+
66+
fn deref(&self) -> &Self::Target {
67+
&self.0
68+
}
69+
}
70+
71+
impl DerefMut for PriceComponentArrayWrapper {
72+
fn deref_mut(&mut self) -> &mut Self::Target {
73+
&mut self.0
74+
}
75+
}
76+
77+
unsafe impl Pod for PriceComponentArrayWrapper {
78+
}
79+
unsafe impl Zeroable for PriceComponentArrayWrapper {
80+
}
81+
3682
/// Pythnet-only extended price account format. This extension is
3783
/// an append-only change that adds extra publisher slots and
3884
/// PriceCumulative for TWAP processing.
3985
#[repr(C)]
4086
#[derive(Copy, Clone, Pod, Zeroable)]
4187
pub struct PriceAccountPythnet {
42-
pub header: AccountHeader,
88+
pub header: AccountHeader,
4389
/// Type of the price account
44-
pub price_type: u32,
90+
pub price_type: u32,
4591
/// Exponent for the published prices
46-
pub exponent: i32,
92+
pub exponent: i32,
4793
/// Current number of authorized publishers
48-
pub num_: u32,
94+
pub num_: u32,
4995
/// Number of valid quotes for the last aggregation
50-
pub num_qt_: u32,
96+
pub num_qt_: u32,
5197
/// Last slot with a succesful aggregation (status : TRADING)
52-
pub last_slot_: u64,
98+
pub last_slot_: u64,
5399
/// Second to last slot where aggregation was attempted
54-
pub valid_slot_: u64,
100+
pub valid_slot_: u64,
55101
/// Ema for price
56-
pub twap_: PriceEma,
102+
pub twap_: PriceEma,
57103
/// Ema for confidence
58-
pub twac_: PriceEma,
104+
pub twac_: PriceEma,
59105
/// Last time aggregation was attempted
60-
pub timestamp_: i64,
106+
pub timestamp_: i64,
61107
/// Minimum valid publisher quotes for a succesful aggregation
62-
pub min_pub_: u8,
63-
pub message_sent_: u8,
108+
pub min_pub_: u8,
109+
pub message_sent_: u8,
64110
/// Configurable max latency in slots between send and receive
65-
pub max_latency_: u8,
111+
pub max_latency_: u8,
66112
/// Unused placeholder for alignment
67-
pub unused_2_: i8,
68-
pub unused_3_: i32,
113+
pub unused_2_: i8,
114+
pub unused_3_: i32,
69115
/// Corresponding product account
70-
pub product_account: Pubkey,
116+
pub product_account: Pubkey,
71117
/// Next price account in the list
72-
pub next_price_account: Pubkey,
118+
pub next_price_account: Pubkey,
73119
/// Second to last slot where aggregation was succesful (i.e. status : TRADING)
74-
pub prev_slot_: u64,
120+
pub prev_slot_: u64,
75121
/// Aggregate price at prev_slot_
76-
pub prev_price_: i64,
122+
pub prev_price_: i64,
77123
/// Confidence interval at prev_slot_
78-
pub prev_conf_: u64,
124+
pub prev_conf_: u64,
79125
/// Timestamp of prev_slot_
80-
pub prev_timestamp_: i64,
126+
pub prev_timestamp_: i64,
81127
/// Last attempted aggregate results
82-
pub agg_: PriceInfo,
128+
pub agg_: PriceInfo,
83129
/// Publishers' price components. NOTE(2023-10-06): On Pythnet, not all
84130
/// PC_NUM_COMP_PYTHNET slots are used due to stack size
85131
/// issues in the C code. For iterating over price components,
86132
/// PC_NUM_COMP must be used.
87-
pub comp_: [PriceComponent; PC_NUM_COMP_PYTHNET as usize],
133+
pub comp_: PriceComponentArrayWrapper,
134+
/// Previous EMA for price and confidence
135+
pub prev_twap_: PriceEma,
136+
pub prev_twac_: PriceEma,
137+
/// Previous TWAP cumulative values
138+
pub prev_price_cumulative: PriceCumulative,
88139
/// Cumulative sums of aggregative price and confidence used to compute arithmetic moving averages
89-
pub price_cumulative: PriceCumulative,
140+
pub price_cumulative: PriceCumulative,
90141
}
91142

92143
impl PriceAccountPythnet {

program/rust/src/processor/upd_price.rs

+73-36
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ use {
22
crate::{
33
accounts::{
44
PriceAccount,
5-
PriceInfo,
65
PythOracleSerialize,
76
UPD_PRICE_WRITE_SEED,
87
},
8+
c_oracle_header::PC_STATUS_TRADING,
99
deserialize::{
1010
load,
1111
load_checked,
@@ -128,7 +128,8 @@ pub fn upd_price(
128128
let clock = Clock::from_account_info(clock_account)?;
129129

130130
let mut publisher_index: usize = 0;
131-
let latest_aggregate_price: PriceInfo;
131+
let slots_since_last_successful_aggregate: u64;
132+
let noninitial_price_update_after_program_upgrade: bool;
132133

133134
// The price_data borrow happens in a scope because it must be
134135
// dropped before we borrow again as raw data pointer for the C
@@ -149,7 +150,16 @@ pub fn upd_price(
149150
OracleError::PermissionViolation.into(),
150151
)?;
151152

152-
latest_aggregate_price = price_data.agg_;
153+
// We use last_slot_ to calculate slots_since_last_successful_aggregate. This is because last_slot_ is updated after the aggregate price is updated successfully.
154+
slots_since_last_successful_aggregate = clock.slot - price_data.last_slot_;
155+
156+
// Check if the program upgrade has happened in the current slot and aggregate price has been updated, if so, use the old logic to update twap/twac/price_cumulative.
157+
// This is to ensure that twap/twac/price_cumulative are calculated correctly during the migration.
158+
// We check if prev_twap_.denom_ is == 0 because when the program upgrade has happened, denom_ is initialized to 0 and it can only stay the same or increase while numer_ can be negative if prices are negative.
159+
// And we check if slots_since_last_successful_aggregate == 0 to check if the aggregate price has been updated in the current slot.
160+
noninitial_price_update_after_program_upgrade =
161+
price_data.prev_twap_.denom_ == 0 && slots_since_last_successful_aggregate == 0;
162+
153163
let latest_publisher_price = price_data.comp_[publisher_index].latest_;
154164

155165
// Check that publisher is publishing a more recent price
@@ -161,10 +171,38 @@ pub fn upd_price(
161171
)?;
162172
}
163173

164-
// Try to update the aggregate
165-
#[allow(unused_variables)]
166-
if clock.slot > latest_aggregate_price.pub_slot_ {
167-
let updated = unsafe {
174+
// Extend the scope of the mutable borrow of price_data
175+
{
176+
let mut price_data = load_checked::<PriceAccount>(price_account, cmd_args.header.version)?;
177+
178+
// Update the publisher's price
179+
if is_component_update(cmd_args)? {
180+
let status: u32 = get_status_for_conf_price_ratio(
181+
cmd_args.price,
182+
cmd_args.confidence,
183+
cmd_args.status,
184+
)?;
185+
let publisher_price = &mut price_data.comp_[publisher_index].latest_;
186+
publisher_price.price_ = cmd_args.price;
187+
publisher_price.conf_ = cmd_args.confidence;
188+
publisher_price.status_ = status;
189+
publisher_price.pub_slot_ = cmd_args.publishing_slot;
190+
}
191+
192+
// If the price update is the first in the slot and the aggregate is trading, update the previous slot, price, conf, and timestamp.
193+
if slots_since_last_successful_aggregate > 0 && price_data.agg_.status_ == PC_STATUS_TRADING
194+
{
195+
price_data.prev_slot_ = price_data.agg_.pub_slot_;
196+
price_data.prev_price_ = price_data.agg_.price_;
197+
price_data.prev_conf_ = price_data.agg_.conf_;
198+
price_data.prev_timestamp_ = price_data.timestamp_;
199+
}
200+
}
201+
202+
let updated = unsafe {
203+
if noninitial_price_update_after_program_upgrade {
204+
false
205+
} else {
168206
// NOTE: c_upd_aggregate must use a raw pointer to price
169207
// data. Solana's `<account>.borrow_*` methods require exclusive
170208
// access, i.e. no other borrow can exist for the account.
@@ -173,25 +211,41 @@ pub fn upd_price(
173211
clock.slot,
174212
clock.unix_timestamp,
175213
)
176-
};
177-
178-
// If the aggregate was successfully updated, calculate the difference and update TWAP.
179-
if updated {
180-
let agg_diff = (clock.slot as i64)
181-
- load_checked::<PriceAccount>(price_account, cmd_args.header.version)?.prev_slot_
182-
as i64;
183-
// Encapsulate TWAP update logic in a function to minimize unsafe block scope.
184-
unsafe {
185-
c_upd_twap(price_account.try_borrow_mut_data()?.as_mut_ptr(), agg_diff);
186-
}
214+
}
215+
};
216+
217+
// If the aggregate was successfully updated, calculate the difference and update TWAP.
218+
if updated {
219+
{
187220
let mut price_data =
188221
load_checked::<PriceAccount>(price_account, cmd_args.header.version)?;
222+
223+
// Multiple price updates may occur within the same slot. Updates within the same slot will
224+
// use the previously calculated values (prev_twap, prev_twac, and prev_price_cumulative)
225+
// from the last successful aggregated price update as their basis for recalculation. This
226+
// ensures that each update within a slot builds upon the last and not the twap/twac/price_cumulative
227+
// that is calculated right after the publishers' individual price updates.
228+
if slots_since_last_successful_aggregate > 0 {
229+
price_data.prev_twap_ = price_data.twap_;
230+
price_data.prev_twac_ = price_data.twac_;
231+
price_data.prev_price_cumulative = price_data.price_cumulative;
232+
}
233+
price_data.twap_ = price_data.prev_twap_;
234+
price_data.twac_ = price_data.prev_twac_;
235+
price_data.price_cumulative = price_data.prev_price_cumulative;
236+
237+
price_data.update_price_cumulative()?;
189238
// We want to send a message every time the aggregate price updates. However, during the migration,
190239
// not every publisher will necessarily provide the accumulator accounts. The message_sent_ flag
191240
// ensures that after every aggregate update, the next publisher who provides the accumulator accounts
192241
// will send the message.
193242
price_data.message_sent_ = 0;
194-
price_data.update_price_cumulative()?;
243+
}
244+
let agg_diff = (clock.slot as i64)
245+
- load_checked::<PriceAccount>(price_account, cmd_args.header.version)?.prev_slot_
246+
as i64;
247+
unsafe {
248+
c_upd_twap(price_account.try_borrow_mut_data()?.as_mut_ptr(), agg_diff);
195249
}
196250
}
197251

@@ -261,23 +315,6 @@ pub fn upd_price(
261315
}
262316
}
263317

264-
// Try to update the publisher's price
265-
if is_component_update(cmd_args)? {
266-
// IMPORTANT: If the publisher does not meet the price/conf
267-
// ratio condition, its price will not count for the next
268-
// aggregate.
269-
let status: u32 =
270-
get_status_for_conf_price_ratio(cmd_args.price, cmd_args.confidence, cmd_args.status)?;
271-
272-
{
273-
let publisher_price = &mut price_data.comp_[publisher_index].latest_;
274-
publisher_price.price_ = cmd_args.price;
275-
publisher_price.conf_ = cmd_args.confidence;
276-
publisher_price.status_ = status;
277-
publisher_price.pub_slot_ = cmd_args.publishing_slot;
278-
}
279-
}
280-
281318
Ok(())
282319
}
283320

program/rust/src/tests/mod.rs

+1-4
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,10 @@ mod test_publish_batch;
1919
mod test_set_max_latency;
2020
mod test_set_min_pub;
2121
mod test_sizes;
22+
mod test_twap;
2223
mod test_upd_aggregate;
2324
mod test_upd_permissions;
2425
mod test_upd_price;
2526
mod test_upd_price_no_fail_on_error;
2627
mod test_upd_product;
2728
mod test_utils;
28-
29-
30-
mod test_twap;
31-
mod test_upd_price_v2;

0 commit comments

Comments
 (0)