Skip to content

Commit 96da017

Browse files
authored
[TWAP] Implement new twap (#359)
* checkpoint * Remove all old sma files * Bring back warnings denial * Restore PRICE_ACCOUNT_SIZE * Checkpoint * Add some tests * First test * Cleanup * Cleanup * Add test sizes * Add another test * Add another test * More comments * Rename * Finish test * Finish test * Delete weird files * Delete resize_price_account * Remove test gating * Rename PRICE_ACCOUNT_SIZE * Renames * Fix tests * Update size check to pythnet * Add comments * set initial size * Refactor update * More tests * Delete outdate comment * Set initial size to the actual size for price accounts * Add size setter for backward compatibility
1 parent f02f8c8 commit 96da017

File tree

15 files changed

+870
-58
lines changed

15 files changed

+870
-58
lines changed

.github/workflows/docker.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: Docker
22

33
on:
44
push:
5+
branches: [ main ]
56
pull_request:
67
branches: [ main ]
78

pc/request.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ void product::update( T *res )
354354
return;
355355
}
356356
pc_prod_t *prod;
357-
size_t plen = std::max( PRICE_ACCOUNT_SIZE, (size_t)PC_PROD_ACC_SIZE );
357+
size_t plen = std::max( ZSTD_UPPER_BOUND, (size_t)PC_PROD_ACC_SIZE );
358358
if ( sizeof( pc_prod_t ) > res->get_data_ref( prod, plen ) ||
359359
prod->magic_ != PC_MAGIC ||
360360
!init_from_account( prod ) ) {
@@ -465,7 +465,7 @@ price::price( const pub_key& acc, product *prod )
465465
preq_->set_account( &apub_ );
466466
areq_->set_sub( this );
467467
preq_->set_sub( this );
468-
size_t tlen = ZSTD_compressBound( PRICE_ACCOUNT_SIZE );
468+
size_t tlen = ZSTD_compressBound( ZSTD_UPPER_BOUND );
469469
pptr_ = (pc_price_t*)new char[tlen];
470470
__builtin_memset( pptr_, 0, tlen );
471471
}
@@ -964,7 +964,7 @@ void price::update( T *res )
964964
}
965965

966966
// get account data
967-
size_t tlen = ZSTD_compressBound( PRICE_ACCOUNT_SIZE );
967+
size_t tlen = ZSTD_compressBound( ZSTD_UPPER_BOUND );
968968
res->get_data_val( pptr_, tlen );
969969
if ( PC_UNLIKELY( pptr_->magic_ != PC_MAGIC ) ) {
970970
on_error_sub( "bad price account header", this );

program/c/src/oracle/oracle.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ extern "C" {
2121
#define PC_PUBKEY_SIZE_64 (PC_PUBKEY_SIZE/sizeof(uint64_t))
2222
#define PC_MAP_TABLE_SIZE 640
2323
#define PC_COMP_SIZE 32
24+
#define PC_COMP_SIZE_V2 128
2425

2526
#define PC_PROD_ACC_SIZE 512
2627
#define PC_EXP_DECAY -9
@@ -200,7 +201,7 @@ static_assert( sizeof( pc_price_t ) == 3312, "" );
200201

201202
// This constant needs to be an upper bound of the price account size, it is used within pythd for ztsd.
202203
// It is set tighly to the current price account + 96 component prices + 48 bytes for cumulative sums
203-
const uint64_t PRICE_ACCOUNT_SIZE = 3312 + 96 * sizeof( pc_price_comp_t) + 48;
204+
const uint64_t ZSTD_UPPER_BOUND = 3312 + 96 * sizeof( pc_price_comp_t) + 48;
204205

205206

206207
// command enumeration

program/rust/src/accounts.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ pub use {
4646
permission::PermissionAccount,
4747
price::{
4848
PriceAccount,
49+
PriceAccountV2,
4950
PriceComponent,
51+
PriceCumulative,
5052
PriceEma,
5153
PriceFeedMessage,
5254
PriceInfo,

program/rust/src/accounts/price.rs

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@ use {
33
AccountHeader,
44
PythAccount,
55
},
6-
crate::c_oracle_header::{
7-
PC_ACCTYPE_PRICE,
8-
PC_COMP_SIZE,
9-
PC_PRICE_T_COMP_OFFSET,
10-
PC_STATUS_TRADING,
6+
crate::{
7+
c_oracle_header::{
8+
PC_ACCTYPE_PRICE,
9+
PC_COMP_SIZE,
10+
PC_COMP_SIZE_V2,
11+
PC_MAX_SEND_LATENCY,
12+
PC_STATUS_TRADING,
13+
},
14+
error::OracleError,
1115
},
1216
bytemuck::{
1317
Pod,
1418
Zeroable,
1519
},
1620
solana_program::pubkey::Pubkey,
21+
std::mem::size_of,
1722
};
1823

1924
#[repr(C)]
@@ -63,6 +68,101 @@ pub struct PriceAccount {
6368
pub comp_: [PriceComponent; PC_COMP_SIZE as usize],
6469
}
6570

71+
/// We are currently out of space in our price accounts. We plan to resize them
72+
/// using the resize_price_account function. Until all the accounts have been resized, all instructions MUST work with either of the two versions.
73+
/// Operations may check the account size to determine whether the old or the new version has been passed.
74+
/// The new price accounts add more publishers and introduce PriceCumulative, a new struct designed to store cumulative sums for computing TWAPs.
75+
#[repr(C)]
76+
#[derive(Copy, Clone, Pod, Zeroable)]
77+
pub struct PriceAccountV2 {
78+
pub header: AccountHeader,
79+
/// Type of the price account
80+
pub price_type: u32,
81+
/// Exponent for the published prices
82+
pub exponent: i32,
83+
/// Current number of authorized publishers
84+
pub num_: u32,
85+
/// Number of valid quotes for the last aggregation
86+
pub num_qt_: u32,
87+
/// Last slot with a succesful aggregation (status : TRADING)
88+
pub last_slot_: u64,
89+
/// Second to last slot where aggregation was attempted
90+
pub valid_slot_: u64,
91+
/// Ema for price
92+
pub twap_: PriceEma,
93+
/// Ema for confidence
94+
pub twac_: PriceEma,
95+
/// Last time aggregation was attempted
96+
pub timestamp_: i64,
97+
/// Minimum valid publisher quotes for a succesful aggregation
98+
pub min_pub_: u8,
99+
pub unused_1_: i8,
100+
pub unused_2_: i16,
101+
pub unused_3_: i32,
102+
/// Corresponding product account
103+
pub product_account: Pubkey,
104+
/// Next price account in the list
105+
pub next_price_account: Pubkey,
106+
/// Second to last slot where aggregation was succesful (i.e. status : TRADING)
107+
pub prev_slot_: u64,
108+
/// Aggregate price at prev_slot_
109+
pub prev_price_: i64,
110+
/// Confidence interval at prev_slot_
111+
pub prev_conf_: u64,
112+
/// Timestamp of prev_slot_
113+
pub prev_timestamp_: i64,
114+
/// Last attempted aggregate results
115+
pub agg_: PriceInfo,
116+
/// Publishers' price components
117+
pub comp_: [PriceComponent; PC_COMP_SIZE as usize],
118+
pub extra_comp_: [PriceComponent; (PC_COMP_SIZE_V2 - PC_COMP_SIZE) as usize], // This space is empty until we update the aggregation to support more pubs
119+
/// Cumulative sums of aggregative price and confidence used to compute arithmetic moving averages
120+
pub price_cumulative: PriceCumulative,
121+
}
122+
123+
impl PriceAccountV2 {
124+
/// This function gets triggered when there's a succesful aggregation and updates the cumulative sums
125+
pub fn update_price_cumulative(&mut self) -> Result<(), OracleError> {
126+
if self.agg_.status_ == PC_STATUS_TRADING {
127+
self.price_cumulative.update(
128+
self.agg_.price_,
129+
self.agg_.conf_,
130+
self.agg_.pub_slot_.saturating_sub(self.prev_slot_),
131+
); // pub_slot should always be >= prev_slot, but we protect ourselves against underflow just in case
132+
Ok(())
133+
} else {
134+
Err(OracleError::NeedsSuccesfulAggregation)
135+
}
136+
}
137+
}
138+
139+
// This struct can't overflow since :
140+
// |sum(price * slotgap)| <= sum(|price * slotgap|) <= max(|price|) * sum(slotgap) <= i64::MAX * * current_slot <= i64::MAX * u64::MAX <= i128::MAX
141+
// |sum(conf * slotgap)| <= sum(|conf * slotgap|) <= max(|conf|) * sum(slotgap) <= u64::MAX * current_slot <= u64::MAX * u64::MAX <= u128::MAX
142+
// num_gaps <= current_slot <= u64::MAX
143+
/// Contains cumulative sums of aggregative price and confidence used to compute arithmetic moving averages.
144+
/// Informally the TWAP between time t and time T can be computed as :
145+
/// `(T.price_cumulative.price - t.price_cumulative.price) / (T.agg_.pub_slot_ - t.agg_.pub_slot_)`
146+
#[repr(C)]
147+
#[derive(Copy, Clone, Pod, Zeroable)]
148+
pub struct PriceCumulative {
149+
pub price: i128, // Cumulative sum of price * slot_gap
150+
pub conf: u128, // Cumulative sum of conf * slot_gap
151+
pub num_gaps: u64, // Cumulative number of gaps in the uptime
152+
pub unused: u64, // Padding for alignment
153+
}
154+
155+
impl PriceCumulative {
156+
pub fn update(&mut self, price: i64, conf: u64, slot_gap: u64) {
157+
self.price += i128::from(price) * i128::from(slot_gap);
158+
self.conf += u128::from(conf) * u128::from(slot_gap);
159+
if slot_gap > PC_MAX_SEND_LATENCY.into() {
160+
self.num_gaps += 1;
161+
}
162+
}
163+
}
164+
165+
66166
#[repr(C)]
67167
#[derive(Copy, Clone, Pod, Zeroable)]
68168
pub struct PriceComponent {
@@ -91,8 +191,12 @@ pub struct PriceEma {
91191

92192
impl PythAccount for PriceAccount {
93193
const ACCOUNT_TYPE: u32 = PC_ACCTYPE_PRICE;
94-
/// Equal to the offset of `comp_` in `PriceAccount`, see the trait comment for more detail
95-
const INITIAL_SIZE: u32 = PC_PRICE_T_COMP_OFFSET as u32;
194+
const INITIAL_SIZE: u32 = size_of::<PriceAccount>() as u32;
195+
}
196+
197+
impl PythAccount for PriceAccountV2 {
198+
const ACCOUNT_TYPE: u32 = PC_ACCTYPE_PRICE;
199+
const INITIAL_SIZE: u32 = size_of::<PriceAccountV2>() as u32;
96200
}
97201

98202
/// Message format for sending data to other chains via the accumulator program

program/rust/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ pub enum OracleError {
5050
InvalidReadableAccount = 618,
5151
#[error("PermissionViolation")]
5252
PermissionViolation = 619,
53+
#[error("NeedsSuccesfulAggregation")]
54+
NeedsSuccesfulAggregation = 620,
5355
}
5456

5557
impl From<OracleError> for ProgramError {

program/rust/src/processor/add_publisher.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use {
33
accounts::{
44
PriceAccount,
55
PriceComponent,
6+
PythAccount,
67
},
78
c_oracle_header::PC_COMP_SIZE,
89
deserialize::{
@@ -26,10 +27,7 @@ use {
2627
program_memory::sol_memset,
2728
pubkey::Pubkey,
2829
},
29-
std::mem::{
30-
size_of,
31-
size_of_val,
32-
},
30+
std::mem::size_of,
3331
};
3432

3533
/// Add publisher to symbol account
@@ -84,8 +82,6 @@ pub fn add_publisher(
8482
);
8583
price_data.comp_[current_index].pub_ = cmd_args.publisher;
8684
price_data.num_ += 1;
87-
price_data.header.size =
88-
try_convert::<_, u32>(size_of::<PriceAccount>() - size_of_val(&price_data.comp_))?
89-
+ price_data.num_ * try_convert::<_, u32>(size_of::<PriceComponent>())?;
85+
price_data.header.size = try_convert::<_, u32>(PriceAccount::INITIAL_SIZE)?;
9086
Ok(())
9187
}

program/rust/src/processor/del_publisher.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use {
33
accounts::{
44
PriceAccount,
55
PriceComponent,
6+
PythAccount,
67
},
78
deserialize::{
89
load,
@@ -25,10 +26,7 @@ use {
2526
program_memory::sol_memset,
2627
pubkey::Pubkey,
2728
},
28-
std::mem::{
29-
size_of,
30-
size_of_val,
31-
},
29+
std::mem::size_of,
3230
};
3331

3432
/// Delete publisher from symbol account
@@ -76,9 +74,7 @@ pub fn del_publisher(
7674
0,
7775
size_of::<PriceComponent>(),
7876
);
79-
price_data.header.size =
80-
try_convert::<_, u32>(size_of::<PriceAccount>() - size_of_val(&price_data.comp_))?
81-
+ price_data.num_ * try_convert::<_, u32>(size_of::<PriceComponent>())?;
77+
price_data.header.size = try_convert::<_, u32>(PriceAccount::INITIAL_SIZE)?;
8278
return Ok(());
8379
}
8480
}

program/rust/src/processor/upd_price.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ use {
22
crate::{
33
accounts::{
44
PriceAccount,
5+
PriceAccountV2,
56
PriceFeedMessage,
67
PriceInfo,
8+
PythAccount,
79
UPD_PRICE_WRITE_SEED,
810
},
911
deserialize::{
@@ -163,6 +165,15 @@ pub fn upd_price(
163165
}
164166
}
165167

168+
{
169+
let account_len = price_account.try_data_len()?;
170+
if aggregate_updated && account_len >= PriceAccountV2::MINIMUM_SIZE {
171+
let mut price_data =
172+
load_checked::<PriceAccountV2>(price_account, cmd_args.header.version)?;
173+
price_data.update_price_cumulative()?;
174+
}
175+
}
176+
166177
let mut price_data = load_checked::<PriceAccount>(price_account, cmd_args.header.version)?;
167178
// We want to send a message every time the aggregate price updates. However, during the migration,
168179
// not every publisher will necessarily provide the accumulator accounts. The message_sent_ flag

program/rust/src/tests/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ mod test_publish;
1717
mod test_publish_batch;
1818
mod test_set_min_pub;
1919
mod test_sizes;
20+
mod test_twap;
2021
mod test_upd_aggregate;
2122
mod test_upd_permissions;
2223
mod test_upd_price;
2324
mod test_upd_price_no_fail_on_error;
25+
mod test_upd_price_v2;
2426
mod test_upd_product;
2527
mod test_utils;

0 commit comments

Comments
 (0)