Skip to content

Commit faec075

Browse files
authored
Add TWAP Message format (#364)
* Yes * Make size smaller * Fix message format * Comments * Refactor * Cleanup
1 parent c2ab474 commit faec075

File tree

4 files changed

+260
-101
lines changed

4 files changed

+260
-101
lines changed

program/rust/src/accounts.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub use {
5656
PriceEma,
5757
PriceFeedMessage,
5858
PriceInfo,
59+
TwapMessage,
5960
},
6061
product::{
6162
read_pc_str_t,

program/rust/src/accounts/price.rs

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ pub struct PriceAccountV2 {
9696
pub timestamp_: i64,
9797
/// Minimum valid publisher quotes for a succesful aggregation
9898
pub min_pub_: u8,
99-
pub unused_1_: i8,
99+
pub message_sent_: u8,
100100
pub unused_2_: i16,
101101
pub unused_3_: i32,
102102
/// Corresponding product account
@@ -120,6 +120,7 @@ pub struct PriceAccountV2 {
120120
pub price_cumulative: PriceCumulative,
121121
}
122122

123+
123124
impl PriceAccountV2 {
124125
/// This function gets triggered when there's a succesful aggregation and updates the cumulative sums
125126
pub fn update_price_cumulative(&mut self) -> Result<(), OracleError> {
@@ -139,7 +140,7 @@ impl PriceAccountV2 {
139140
// This struct can't overflow since :
140141
// |sum(price * slotgap)| <= sum(|price * slotgap|) <= max(|price|) * sum(slotgap) <= i64::MAX * * current_slot <= i64::MAX * u64::MAX <= i128::MAX
141142
// |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+
// num_down_slots <= current_slot <= u64::MAX
143144
/// Contains cumulative sums of aggregative price and confidence used to compute arithmetic moving averages.
144145
/// Informally the TWAP between time t and time T can be computed as :
145146
/// `(T.price_cumulative.price - t.price_cumulative.price) / (T.agg_.pub_slot_ - t.agg_.pub_slot_)`
@@ -240,7 +241,7 @@ impl PriceFeedMessage {
240241
pub const MESSAGE_SIZE: usize = 1 + 32 + 8 + 8 + 4 + 8 + 8 + 8 + 8;
241242
pub const DISCRIMINATOR: u8 = 0;
242243

243-
pub fn from_price_account(key: &Pubkey, account: &PriceAccount) -> Self {
244+
pub fn from_price_account(key: &Pubkey, account: &PriceAccountV2) -> Self {
244245
let (price, conf, publish_time) = if account.agg_.status_ == PC_STATUS_TRADING {
245246
(account.agg_.price_, account.agg_.conf_, account.timestamp_)
246247
} else {
@@ -267,12 +268,12 @@ impl PriceFeedMessage {
267268
/// Note that it would be more idiomatic to return a `Vec`, but that approach adds
268269
/// to the size of the compiled binary (which is already close to the size limit).
269270
#[allow(unused_assignments)]
270-
pub fn as_bytes(&self) -> [u8; PriceFeedMessage::MESSAGE_SIZE] {
271-
let mut bytes = [0u8; PriceFeedMessage::MESSAGE_SIZE];
271+
pub fn as_bytes(&self) -> [u8; Self::MESSAGE_SIZE] {
272+
let mut bytes = [0u8; Self::MESSAGE_SIZE];
272273

273274
let mut i: usize = 0;
274275

275-
bytes[i..i + 1].clone_from_slice(&[PriceFeedMessage::DISCRIMINATOR]);
276+
bytes[i..i + 1].clone_from_slice(&[Self::DISCRIMINATOR]);
276277
i += 1;
277278

278279
bytes[i..i + 32].clone_from_slice(&self.id[..]);
@@ -302,3 +303,82 @@ impl PriceFeedMessage {
302303
bytes
303304
}
304305
}
306+
307+
/// Message format for sending Twap data via the accumulator program
308+
#[repr(C)]
309+
#[derive(Debug, Copy, Clone, PartialEq)]
310+
pub struct TwapMessage {
311+
pub id: [u8; 32],
312+
pub cumulative_price: i128,
313+
pub cumulative_conf: u128,
314+
pub num_down_slots: u64,
315+
pub exponent: i32,
316+
pub publish_time: i64,
317+
pub prev_publish_time: i64,
318+
pub publish_slot: u64,
319+
}
320+
321+
impl TwapMessage {
322+
// The size of the serialized message. Note that this is not the same as the size of the struct
323+
// (because of the discriminator & struct padding/alignment).
324+
pub const MESSAGE_SIZE: usize = 1 + 32 + 16 + 16 + 8 + 4 + 8 + 8 + 8;
325+
pub const DISCRIMINATOR: u8 = 1;
326+
327+
pub fn from_price_account(key: &Pubkey, account: &PriceAccountV2) -> Self {
328+
let publish_time = if account.agg_.status_ == PC_STATUS_TRADING {
329+
account.timestamp_
330+
} else {
331+
account.prev_timestamp_
332+
};
333+
334+
Self {
335+
id: key.to_bytes(),
336+
cumulative_price: account.price_cumulative.price,
337+
cumulative_conf: account.price_cumulative.conf,
338+
num_down_slots: account.price_cumulative.num_down_slots,
339+
exponent: account.exponent,
340+
publish_time,
341+
prev_publish_time: account.prev_timestamp_,
342+
publish_slot: account.last_slot_,
343+
}
344+
}
345+
346+
/// Serialize this message as an array of bytes (including the discriminator)
347+
/// Note that it would be more idiomatic to return a `Vec`, but that approach adds
348+
/// to the size of the compiled binary (which is already close to the size limit).
349+
#[allow(unused_assignments)]
350+
pub fn as_bytes(&self) -> [u8; Self::MESSAGE_SIZE] {
351+
let mut bytes = [0u8; Self::MESSAGE_SIZE];
352+
353+
let mut i: usize = 0;
354+
355+
bytes[i..i + 1].clone_from_slice(&[Self::DISCRIMINATOR]);
356+
i += 1;
357+
358+
bytes[i..i + 32].clone_from_slice(&self.id[..]);
359+
i += 32;
360+
361+
bytes[i..i + 16].clone_from_slice(&self.cumulative_price.to_be_bytes());
362+
i += 16;
363+
364+
bytes[i..i + 16].clone_from_slice(&self.cumulative_conf.to_be_bytes());
365+
i += 16;
366+
367+
bytes[i..i + 8].clone_from_slice(&self.num_down_slots.to_be_bytes());
368+
i += 8;
369+
370+
bytes[i..i + 4].clone_from_slice(&self.exponent.to_be_bytes());
371+
i += 4;
372+
373+
bytes[i..i + 8].clone_from_slice(&self.publish_time.to_be_bytes());
374+
i += 8;
375+
376+
bytes[i..i + 8].clone_from_slice(&self.prev_publish_time.to_be_bytes());
377+
i += 8;
378+
379+
bytes[i..i + 8].clone_from_slice(&self.publish_slot.to_be_bytes());
380+
i += 8;
381+
382+
bytes
383+
}
384+
}

program/rust/src/processor/upd_price.rs

Lines changed: 65 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use {
66
PriceFeedMessage,
77
PriceInfo,
88
PythAccount,
9+
TwapMessage,
910
UPD_PRICE_WRITE_SEED,
1011
},
1112
deserialize::{
@@ -152,10 +153,8 @@ pub fn upd_price(
152153
}
153154

154155
// Try to update the aggregate
155-
#[allow(unused_variables)]
156156
let mut aggregate_updated = false;
157157
if clock.slot > latest_aggregate_price.pub_slot_ {
158-
#[allow(unused_assignments)]
159158
unsafe {
160159
aggregate_updated = c_upd_aggregate(
161160
price_account.try_borrow_mut_data()?.as_mut_ptr(),
@@ -165,82 +164,86 @@ pub fn upd_price(
165164
}
166165
}
167166

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)?;
167+
168+
let account_len = price_account.try_data_len()?;
169+
if account_len >= PriceAccountV2::MINIMUM_SIZE {
170+
let mut price_data =
171+
load_checked::<PriceAccountV2>(price_account, cmd_args.header.version)?;
172+
173+
if aggregate_updated {
173174
price_data.update_price_cumulative()?;
175+
// We want to send a message every time the aggregate price updates. However, during the migration,
176+
// not every publisher will necessarily provide the accumulator accounts. The message_sent_ flag
177+
// ensures that after every aggregate update, the next publisher who provides the accumulator accounts
178+
// will send the message.
179+
price_data.message_sent_ = 0;
174180
}
175-
}
176181

177-
let mut price_data = load_checked::<PriceAccount>(price_account, cmd_args.header.version)?;
178-
// We want to send a message every time the aggregate price updates. However, during the migration,
179-
// not every publisher will necessarily provide the accumulator accounts. The message_sent_ flag
180-
// ensures that after every aggregate update, the next publisher who provides the accumulator accounts
181-
// will send the message.
182-
if aggregate_updated {
183-
price_data.message_sent_ = 0;
184-
}
185182

186-
if let Some(accumulator_accounts) = maybe_accumulator_accounts {
187-
if price_data.message_sent_ == 0 {
188-
// Check that the oracle PDA is correctly configured for the program we are calling.
189-
let oracle_auth_seeds: &[&[u8]] = &[
190-
UPD_PRICE_WRITE_SEED.as_bytes(),
191-
&accumulator_accounts.program_id.key.to_bytes(),
192-
];
193-
let (expected_oracle_auth_pda, bump) =
194-
Pubkey::find_program_address(oracle_auth_seeds, program_id);
195-
pyth_assert(
196-
expected_oracle_auth_pda == *accumulator_accounts.oracle_auth_pda.key,
197-
OracleError::InvalidPda.into(),
198-
)?;
183+
if let Some(accumulator_accounts) = maybe_accumulator_accounts {
184+
if price_data.message_sent_ == 0 {
185+
// Check that the oracle PDA is correctly configured for the program we are calling.
186+
let oracle_auth_seeds: &[&[u8]] = &[
187+
UPD_PRICE_WRITE_SEED.as_bytes(),
188+
&accumulator_accounts.program_id.key.to_bytes(),
189+
];
190+
let (expected_oracle_auth_pda, bump) =
191+
Pubkey::find_program_address(oracle_auth_seeds, program_id);
192+
pyth_assert(
193+
expected_oracle_auth_pda == *accumulator_accounts.oracle_auth_pda.key,
194+
OracleError::InvalidPda.into(),
195+
)?;
199196

200-
let account_metas = vec![
201-
AccountMeta {
202-
pubkey: *accumulator_accounts.whitelist.key,
203-
is_signer: false,
204-
is_writable: false,
205-
},
206-
AccountMeta {
207-
pubkey: *accumulator_accounts.oracle_auth_pda.key,
208-
is_signer: true,
209-
is_writable: false,
210-
},
211-
AccountMeta {
212-
pubkey: *accumulator_accounts.message_buffer_data.key,
213-
is_signer: false,
214-
is_writable: true,
215-
},
216-
];
197+
let account_metas = vec![
198+
AccountMeta {
199+
pubkey: *accumulator_accounts.whitelist.key,
200+
is_signer: false,
201+
is_writable: false,
202+
},
203+
AccountMeta {
204+
pubkey: *accumulator_accounts.oracle_auth_pda.key,
205+
is_signer: true,
206+
is_writable: false,
207+
},
208+
AccountMeta {
209+
pubkey: *accumulator_accounts.message_buffer_data.key,
210+
is_signer: false,
211+
is_writable: true,
212+
},
213+
];
217214

218-
let message =
219-
vec![
215+
let message = vec![
220216
PriceFeedMessage::from_price_account(price_account.key, &price_data)
221217
.as_bytes()
222218
.to_vec(),
219+
TwapMessage::from_price_account(price_account.key, &price_data)
220+
.as_bytes()
221+
.to_vec(),
223222
];
224223

225-
// anchor discriminator for "global:put_all"
226-
let discriminator = [212, 225, 193, 91, 151, 238, 20, 93];
227-
let create_inputs_ix = Instruction::new_with_borsh(
228-
*accumulator_accounts.program_id.key,
229-
&(discriminator, price_account.key.to_bytes(), message),
230-
account_metas,
231-
);
224+
// anchor discriminator for "global:put_all"
225+
let discriminator = [212, 225, 193, 91, 151, 238, 20, 93];
226+
let create_inputs_ix = Instruction::new_with_borsh(
227+
*accumulator_accounts.program_id.key,
228+
&(discriminator, price_account.key.to_bytes(), message),
229+
account_metas,
230+
);
232231

233-
let auth_seeds_with_bump: &[&[u8]] = &[
234-
UPD_PRICE_WRITE_SEED.as_bytes(),
235-
&accumulator_accounts.program_id.key.to_bytes(),
236-
&[bump],
237-
];
232+
let auth_seeds_with_bump: &[&[u8]] = &[
233+
UPD_PRICE_WRITE_SEED.as_bytes(),
234+
&accumulator_accounts.program_id.key.to_bytes(),
235+
&[bump],
236+
];
238237

239-
invoke_signed(&create_inputs_ix, accounts, &[auth_seeds_with_bump])?;
240-
price_data.message_sent_ = 1;
238+
invoke_signed(&create_inputs_ix, accounts, &[auth_seeds_with_bump])?;
239+
price_data.message_sent_ = 1;
240+
}
241241
}
242242
}
243243

244+
245+
let mut price_data = load_checked::<PriceAccount>(price_account, cmd_args.header.version)?;
246+
244247
// Try to update the publisher's price
245248
if is_component_update(cmd_args)? {
246249
let status: u32 =

0 commit comments

Comments
 (0)