From 5dbd3ea7224d10ec116b7ba35ee246b7cd342a22 Mon Sep 17 00:00:00 2001 From: Stan Drozd Date: Mon, 2 Aug 2021 13:20:06 +0200 Subject: [PATCH] pyth2wormhole: on-chain attestation call, update Pyth-facing types This commit takes the selected Pyth Price struct account and after serialization places them inside a PostMessage cross-program call to Wormhole. Change-Id: If04123705290f4749de318c0dfaa8f1d840ed349 --- solana/bridge/program/src/types.rs | 4 +- solana/pyth2wormhole/Cargo.lock | 4 +- solana/pyth2wormhole/program/Cargo.toml | 4 +- solana/pyth2wormhole/program/src/attest.rs | 172 +++++++++ solana/pyth2wormhole/program/src/config.rs | 8 +- solana/pyth2wormhole/program/src/forward.rs | 73 ---- .../pyth2wormhole/program/src/initialize.rs | 13 +- solana/pyth2wormhole/program/src/lib.rs | 26 +- .../pyth2wormhole/program/src/set_config.rs | 2 +- solana/pyth2wormhole/program/src/types.rs | 165 --------- solana/pyth2wormhole/program/src/types/mod.rs | 331 ++++++++++++++++++ .../program/src/types/pyth_extensions.rs | 139 ++++++++ solana/solitaire/program/src/lib.rs | 2 + solana/solitaire/rocksalt/src/lib.rs | 2 +- 14 files changed, 675 insertions(+), 270 deletions(-) create mode 100644 solana/pyth2wormhole/program/src/attest.rs delete mode 100644 solana/pyth2wormhole/program/src/forward.rs delete mode 100644 solana/pyth2wormhole/program/src/types.rs create mode 100644 solana/pyth2wormhole/program/src/types/mod.rs create mode 100644 solana/pyth2wormhole/program/src/types/pyth_extensions.rs diff --git a/solana/bridge/program/src/types.rs b/solana/bridge/program/src/types.rs index 190eebb3..e33954e9 100644 --- a/solana/bridge/program/src/types.rs +++ b/solana/bridge/program/src/types.rs @@ -71,7 +71,7 @@ impl Owned for GuardianSetData { } } -#[derive(Default, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[derive(Clone, Default, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] pub struct BridgeConfig { /// Period for how long a guardian set is valid after it has been replaced by a new one. This /// guarantees that VAAs issued by that set can still be submitted for a certain period. In @@ -82,7 +82,7 @@ pub struct BridgeConfig { pub fee: u64, } -#[derive(Default, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[derive(Clone, Default, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] pub struct BridgeData { /// The current guardian set index, used to decide which signature sets to accept. pub guardian_set_index: u32, diff --git a/solana/pyth2wormhole/Cargo.lock b/solana/pyth2wormhole/Cargo.lock index 313b7ff5..67482c5f 100644 --- a/solana/pyth2wormhole/Cargo.lock +++ b/solana/pyth2wormhole/Cargo.lock @@ -2057,8 +2057,8 @@ dependencies = [ [[package]] name = "pyth-client" -version = "0.2.0" -source = "git+https://github.com/drozdziak1/pyth-client-rs?branch=v2-clone-and-debug#0b073bcdad1312051b086334e00f7925cff137e2" +version = "0.2.2" +source = "git+https://github.com/pyth-network/pyth-client-rs?branch=v2#0d2689fdd4ffba09d7d77f5a52b09e16912983ec" [[package]] name = "pyth2wormhole" diff --git a/solana/pyth2wormhole/program/Cargo.toml b/solana/pyth2wormhole/program/Cargo.toml index 8239c9ec..0725e1e6 100644 --- a/solana/pyth2wormhole/program/Cargo.toml +++ b/solana/pyth2wormhole/program/Cargo.toml @@ -10,13 +10,15 @@ name = "pyth2wormhole" [features] client = ["solitaire/client", "solitaire-client", "no-entrypoint"] +trace = ["solitaire/trace", "bridge/trace"] no-entrypoint = [] [dependencies] bridge = {path = "../../bridge/program"} solitaire = { path = "../../solitaire/program" } solitaire-client = { path = "../../solitaire/client", optional = true } -pyth-client = {git = "https://github.com/drozdziak1/pyth-client-rs", branch = "v2-clone-and-debug"} rocksalt = { path = "../../solitaire/rocksalt" } solana-program = "=1.7.0" borsh = "0.8.1" +# NOTE: We're following bleeding edge to encounter format changes more easily +pyth-client = {git = "https://github.com/pyth-network/pyth-client-rs", branch = "v2"} diff --git a/solana/pyth2wormhole/program/src/attest.rs b/solana/pyth2wormhole/program/src/attest.rs new file mode 100644 index 00000000..e27296bc --- /dev/null +++ b/solana/pyth2wormhole/program/src/attest.rs @@ -0,0 +1,172 @@ +use crate::{ + config::P2WConfigAccount, + types::PriceAttestation, +}; +use borsh::{ + BorshDeserialize, + BorshSerialize, +}; +use solana_program::{ + clock::Clock, + instruction::{ + AccountMeta, + Instruction, + }, + program::{ + invoke, + invoke_signed, + }, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, +}; + +use bridge::{ + types::{ + BridgeData, + ConsistencyLevel, + }, + PostMessageData, +}; + +use solitaire::{ + trace, + AccountState, + Derive, + ExecutionContext, + FromAccounts, + Info, + InstructionContext, + Keyed, + Mut, + Peel, + Result as SoliResult, + Seeded, + Signer, + SolitaireError, + Sysvar, + ToInstruction, +}; + +#[derive(FromAccounts, ToInstruction)] +pub struct Attest<'b> { + // Payer also used for wormhole + pub payer: Mut>>, + pub system_program: Info<'b>, + pub config: P2WConfigAccount<'b, { AccountState::Initialized }>, + pub wormhole_program: Info<'b>, + pub pyth_product: Info<'b>, + pub pyth_price: Info<'b>, + pub clock: Sysvar<'b, Clock>, + + // post_message accounts + /// Wormhole program address + pub wh_prog: Info<'b>, + + /// Bridge config needed for fee calculation + pub wh_bridge: Mut>, + + /// Account to store the posted message + pub wh_message: Signer>>, + + /// Emitter of the VAA + pub wh_emitter: Info<'b>, + + /// Tracker for the emitter sequence + pub wh_sequence: Mut>, + + // We reuse our payer + // pub wh_payer: Mut>>, + /// Account to collect tx fee + pub wh_fee_collector: Mut>, + + pub wh_rent: Sysvar<'b, Rent>, +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct AttestData { + pub nonce: u32, + pub consistency_level: ConsistencyLevel, +} + +impl<'b> InstructionContext<'b> for Attest<'b> { + fn deps(&self) -> Vec { + vec![solana_program::system_program::id()] + } +} + +pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> SoliResult<()> { + accs.config.verify_derivation(ctx.program_id, None)?; + + if accs.config.pyth_owner != *accs.pyth_price.owner + || accs.config.pyth_owner != *accs.pyth_product.owner + { + trace!(&format!( + "pyth_owner pubkey mismatch (expected {:?}, got price owner {:?} and product owner {:?}", + accs.config.pyth_owner, accs.pyth_price.owner, accs.pyth_product.owner + )); + return Err(SolitaireError::InvalidOwner(accs.pyth_price.owner.clone()).into()); + } + + if accs.config.wh_prog != *accs.wh_prog.key { + trace!(&format!( + "Wormhole program account mismatch (expected {:?}, got {:?})", + accs.config.wh_prog, accs.wh_prog.key + )); + } + + let price_attestation = PriceAttestation::from_pyth_price_bytes( + accs.pyth_price.key.clone(), + accs.clock.unix_timestamp, + &*accs.pyth_price.try_borrow_data()?, + )?; + + if &price_attestation.product_id != accs.pyth_product.key { + trace!(&format!( + "Price's product_id does not match the pased account (points at {:?} instead)", + price_attestation.product_id + )); + return Err(ProgramError::InvalidAccountData.into()); + } + + let bridge_config = BridgeData::try_from_slice(&accs.wh_bridge.try_borrow_mut_data()?)?.config; + + // Pay wormhole fee + let transfer_ix = solana_program::system_instruction::transfer( + accs.payer.key, + accs.wh_fee_collector.info().key, + bridge_config.fee, + ); + solana_program::program::invoke(&transfer_ix, ctx.accounts)?; + + // Send payload + let post_message_data = ( + bridge::instruction::Instruction::PostMessage, + PostMessageData { + nonce: data.nonce, + payload: price_attestation.serialize(), + consistency_level: data.consistency_level, + }, + ); + + let ix = Instruction::new_with_bytes( + accs.config.wh_prog, + post_message_data.try_to_vec()?.as_slice(), + vec![ + AccountMeta::new(*accs.wh_bridge.key, false), + AccountMeta::new(*accs.wh_message.key, true), + AccountMeta::new_readonly(*accs.wh_emitter.key, true), + AccountMeta::new(*accs.wh_sequence.key, false), + AccountMeta::new(*accs.payer.key, true), + AccountMeta::new(*accs.wh_fee_collector.key, false), + AccountMeta::new_readonly(*accs.clock.info().key, false), + AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + ], + ); + + trace!("Before cross-call"); + invoke(&ix, ctx.accounts)?; + + Ok(()) +} diff --git a/solana/pyth2wormhole/program/src/config.rs b/solana/pyth2wormhole/program/src/config.rs index 3c5952f6..56d75897 100644 --- a/solana/pyth2wormhole/program/src/config.rs +++ b/solana/pyth2wormhole/program/src/config.rs @@ -1,17 +1,13 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::pubkey::Pubkey; -use solitaire::{ - data_wrapper, - processors::seeded::{AccountOwner, Seeded}, - AccountState, Data, Derive, Owned, -}; +use solitaire::{processors::seeded::AccountOwner, AccountState, Data, Derive, Owned}; #[derive(Default, BorshDeserialize, BorshSerialize)] pub struct Pyth2WormholeConfig { /// Authority owning this contract pub owner: Pubkey, /// Wormhole bridge program - pub wormhole_program_addr: Pubkey, + pub wh_prog: Pubkey, /// Authority owning Pyth price data pub pyth_owner: Pubkey, } diff --git a/solana/pyth2wormhole/program/src/forward.rs b/solana/pyth2wormhole/program/src/forward.rs deleted file mode 100644 index ba31ae94..00000000 --- a/solana/pyth2wormhole/program/src/forward.rs +++ /dev/null @@ -1,73 +0,0 @@ - - -use borsh::{BorshDeserialize, BorshSerialize}; - -use solana_program::{msg, program_error::ProgramError, pubkey::Pubkey}; -use solitaire::{ - processors::seeded::AccountOwner, AccountState, Context, Data, ExecutionContext, FromAccounts, - Info, InstructionContext, Keyed, Owned, Peel, Result as SoliResult, Signer, - ToInstruction, -}; - -use crate::{config::P2WConfigAccount, types::PriceAttestation}; -use solana_program::{ - clock::Clock, instruction::Instruction, msg, program::invoke_signed, - program_error::ProgramError, pubkey::Pubkey, -}; -use solitaire::{ - processors::seeded::AccountOwner, trace, AccountState, CPICall, Context, Data, - ExecutionContext, FromAccounts, Info, InstructionContext, Keyed, Owned, Peel, - Result as SoliResult, Signer, SolitaireError, Sysvar, ToInstruction, -}; - -#[derive(FromAccounts, ToInstruction)] -pub struct Forward<'b> { - pub payer: Signer>, - pub system_program: Signer>, - pub config: P2WConfigAccount<'b, {AccountState::Initialized}>, - pub wormhole_program: Info<'b>, - pub pyth_product: Info<'b>, - pub pyth_price: Info<'b>, - pub post_message_call: CPICall>, -} - -#[derive(BorshDeserialize, BorshSerialize)] -pub struct ForwardData { - pub target_chain: u32, -} - -impl<'b> InstructionContext<'b> for Forward<'b> { - fn verify(&self, _program_id: &Pubkey) -> SoliResult<()> { - if self.config.wormhole_program_addr != *self.wormhole_program.key { - trace!(&format!( - "wormhole_program pubkey mismatch (expected {:?}", - self.config.wormhole_program_addr - )); - return Err(ProgramError::InvalidAccountData.into()); - } - if self.config.pyth_owner != *self.pyth_price.owner - || self.config.pyth_owner != *self.pyth_product.owner - { - trace!(&format!( - "pyth_owner pubkey mismatch (expected {:?}", - self.config.pyth_owner - )); - return Err(SolitaireError::InvalidOwner(self.pyth_price.owner.clone()).into()); - } - Ok(()) - } - - fn deps(&self) -> Vec { - vec![solana_program::system_program::id()] - } -} - -pub fn forward_price( - _ctx: &ExecutionContext, - accs: &mut Forward, - _data: ForwardData, -) -> SoliResult<()> { - let _price_attestation = PriceAttestation::from_bytes(&*accs.pyth_price.0.try_borrow_data()?)?; - - Ok(()) -} diff --git a/solana/pyth2wormhole/program/src/initialize.rs b/solana/pyth2wormhole/program/src/initialize.rs index cbeb0351..94ae4357 100644 --- a/solana/pyth2wormhole/program/src/initialize.rs +++ b/solana/pyth2wormhole/program/src/initialize.rs @@ -1,22 +1,15 @@ use solana_program::pubkey::Pubkey; -use solitaire::{ - AccountState, Context, Creatable, CreationLamports, ExecutionContext, FromAccounts, Info, - InstructionContext, Keyed, Peel, Result as SoliResult, Signer, ToInstruction, -}; +use solitaire::{AccountState, CreationLamports, ExecutionContext, FromAccounts, Info, InstructionContext, Keyed, Mut, Peel, Result as SoliResult, Signer, ToInstruction}; use crate::config::{P2WConfigAccount, Pyth2WormholeConfig}; #[derive(FromAccounts, ToInstruction)] pub struct Initialize<'b> { - pub new_config: P2WConfigAccount<'b, {AccountState::Uninitialized}>, - pub payer: Signer>, + pub new_config: Mut>, + pub payer: Mut>>, } impl<'b> InstructionContext<'b> for Initialize<'b> { - fn verify(&self, _program_id: &Pubkey) -> SoliResult<()> { - Ok(()) - } - fn deps(&self) -> Vec { vec![] } diff --git a/solana/pyth2wormhole/program/src/lib.rs b/solana/pyth2wormhole/program/src/lib.rs index c99205a0..05ecc3ee 100644 --- a/solana/pyth2wormhole/program/src/lib.rs +++ b/solana/pyth2wormhole/program/src/lib.rs @@ -1,21 +1,29 @@ #![feature(const_generics)] +pub mod attest; pub mod config; -pub mod forward; pub mod initialize; pub mod set_config; pub mod types; -use solitaire::{ - solitaire +use solitaire::solitaire; + +pub use attest::{ + attest, + Attest, + AttestData, +}; +pub use config::Pyth2WormholeConfig; +pub use initialize::{ + initialize, + Initialize, +}; +pub use set_config::{ + set_config, + SetConfig, }; -pub use config::Pyth2WormholeConfig; -pub use forward::{forward_price, Forward, ForwardData}; -pub use initialize::{initialize, Initialize}; -pub use set_config::{set_config, SetConfig}; - solitaire! { - Forward(ForwardData) => forward_price, + Attest(AttestData) => attest, Initialize(Pyth2WormholeConfig) => initialize, SetConfig(Pyth2WormholeConfig) => set_config, } diff --git a/solana/pyth2wormhole/program/src/set_config.rs b/solana/pyth2wormhole/program/src/set_config.rs index 62d51234..32642de7 100644 --- a/solana/pyth2wormhole/program/src/set_config.rs +++ b/solana/pyth2wormhole/program/src/set_config.rs @@ -1,6 +1,6 @@ use solana_program::{msg, pubkey::Pubkey}; use solitaire::{ - AccountState, Context, ExecutionContext, FromAccounts, Info, InstructionContext, Keyed, Peel, + AccountState, ExecutionContext, FromAccounts, Info, InstructionContext, Keyed, Peel, Result as SoliResult, Signer, SolitaireError, ToInstruction, }; diff --git a/solana/pyth2wormhole/program/src/types.rs b/solana/pyth2wormhole/program/src/types.rs deleted file mode 100644 index 3094b851..00000000 --- a/solana/pyth2wormhole/program/src/types.rs +++ /dev/null @@ -1,165 +0,0 @@ -use std::{mem}; - -use pyth_client::{CorpAction, Price, PriceStatus, PriceType}; -use solana_program::{program_error::ProgramError, pubkey::Pubkey}; -use solitaire::{Result as SoliResult, SolitaireError}; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PriceAttestation { - pub product: Pubkey, - pub price_type: PriceType, - pub price: i64, - pub expo: i32, - pub confidence_interval: u64, - pub status: PriceStatus, - pub corp_act: CorpAction, -} - -impl PriceAttestation { - pub fn from_bytes(value: &[u8]) -> Result { - let price = parse_pyth_price(value)?; - - Ok(PriceAttestation { - product: Pubkey::new(&price.prod.val[..]), - price_type: price.ptype, - price: price.agg.price, - expo: price.expo, - confidence_interval: price.agg.conf, - status: price.agg.status, - corp_act: price.agg.corp_act, - }) - } -} - -/// Deserializes Price from raw bytes -fn parse_pyth_price(price_data: &[u8]) -> SoliResult { - if price_data.len() != mem::size_of::() { - return Err(ProgramError::InvalidAccountData.into()); - } - let price_account = pyth_client::cast::(price_data); - - Ok(price_account.clone()) -} - -#[cfg(test)] -mod tests { - use super::*; - use pyth_client::{AccKey, AccountType, PriceComp, PriceInfo}; - - macro_rules! empty_acckey { - () => { - AccKey { val: [0u8; 32] } - }; - } - - macro_rules! empty_priceinfo { - () => { - PriceInfo { - price: 0, - conf: 0, - status: PriceStatus::Unknown, - corp_act: CorpAction::NoCorpAct, - pub_slot: 0, - } - }; - } - - macro_rules! empty_pricecomp { - () => { - PriceComp { - publisher: empty_acckey!(), - agg: empty_priceinfo!(), - latest: empty_priceinfo!(), - } - }; - } - - macro_rules! empty_price { - () => { - Price { - magic: pyth_client::MAGIC, - ver: pyth_client::VERSION, - atype: AccountType::Price as u32, - size: 0, - ptype: PriceType::Price, - expo: 0, - num: 0, - unused: 0, - curr_slot: 0, - valid_slot: 0, - twap: 0, - avol: 0, - drv0: 0, - drv1: 0, - drv2: 0, - drv3: 0, - drv4: 0, - drv5: 0, - prod: empty_acckey!(), - next: empty_acckey!(), - agg_pub: empty_acckey!(), - agg: empty_priceinfo!(), - // A nice macro might fix come handy if this gets annoying - comp: [ - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - empty_pricecomp!(), - ], - } - }; - } - - #[test] - fn test_parse_pyth_price_wrong_size_slices() { - assert!(parse_pyth_price(&[]).is_err()); - assert!(parse_pyth_price(vec![0u8; 1].as_slice()).is_err()); - } - - #[test] - fn test_normal_values() -> SoliResult<()> { - let price = Price { - expo: 5, - agg: PriceInfo { - price: 42, - ..empty_priceinfo!() - }, - ..empty_price!() - }; - let price_vec = vec![price.clone()]; - - // use the C repr to mock pyth's format - let (_, bytes, _) = unsafe { price_vec.as_slice().align_to::() }; - - assert_eq!(parse_pyth_price(bytes)?, price); - Ok(()) - } -} diff --git a/solana/pyth2wormhole/program/src/types/mod.rs b/solana/pyth2wormhole/program/src/types/mod.rs new file mode 100644 index 00000000..90aab032 --- /dev/null +++ b/solana/pyth2wormhole/program/src/types/mod.rs @@ -0,0 +1,331 @@ +pub mod pyth_extensions; + +use std::mem; + +use borsh::BorshSerialize; +use pyth_client::{ + AccountType, + CorpAction, + Ema, + Price, + PriceStatus, + PriceType, +}; +use solana_program::{clock::UnixTimestamp, program_error::ProgramError, pubkey::Pubkey}; +use solitaire::{ + trace, + Result as SoliResult, + SolitaireError, +}; + +use self::pyth_extensions::{ + P2WCorpAction, + P2WEma, + P2WPriceStatus, + P2WPriceType, +}; + +// Constants and values common to every p2w custom-serialized message + +/// Precedes every message implementing the p2w serialization format +pub const P2W_MAGIC: &'static [u8] = b"P2WH"; + +/// Format version used and understood by this codebase +pub const P2W_FORMAT_VERSION: u16 = 1; + +/// Decides the format of following bytes +#[repr(u8)] +pub enum PayloadId { + PriceAttestation = 1, +} + +// On-chain data types + +#[derive(Clone, Default, Debug, Eq, PartialEq)] +pub struct PriceAttestation { + pub product_id: Pubkey, + pub price_id: Pubkey, + pub price_type: P2WPriceType, + pub price: i64, + pub expo: i32, + pub twap: P2WEma, + pub twac: P2WEma, + pub confidence_interval: u64, + pub status: P2WPriceStatus, + pub corp_act: P2WCorpAction, + pub timestamp: UnixTimestamp, +} + +impl PriceAttestation { + pub fn from_pyth_price_bytes(price_id: Pubkey, timestamp: UnixTimestamp, value: &[u8]) -> Result { + let price = parse_pyth_price(value)?; + + Ok(PriceAttestation { + product_id: Pubkey::new(&price.prod.val[..]), + price_id, + price_type: (&price.ptype).into(), + price: price.agg.price, + twap: (&price.twap).into(), + twac: (&price.twac).into(), + expo: price.expo, + confidence_interval: price.agg.conf, + status: (&price.agg.status).into(), + corp_act: (&price.agg.corp_act).into(), + timestamp: timestamp, + }) + } + + /// Serialize this attestation according to the Pyth-over-wormhole serialization format + pub fn serialize(&self) -> Vec { + // A nifty trick to get us yelled at if we forget to serialize a field + #[deny(warnings)] + let PriceAttestation { + product_id, + price_id, + price_type, + price, + expo, + twap, + twac, + confidence_interval, + status, + corp_act, + timestamp + } = self; + + // magic + let mut buf = P2W_MAGIC.to_vec(); + + // version + buf.extend_from_slice(&P2W_FORMAT_VERSION.to_be_bytes()[..]); + + // payload_id + buf.push(PayloadId::PriceAttestation as u8); + + // product_id + buf.extend_from_slice(&product_id.to_bytes()[..]); + + // price_id + buf.extend_from_slice(&price_id.to_bytes()[..]); + + // price_type + buf.push(price_type.clone() as u8); + + // price + buf.extend_from_slice(&price.to_be_bytes()[..]); + + // exponent + buf.extend_from_slice(&expo.to_be_bytes()[..]); + + // twap + buf.append(&mut twap.serialize()); + + // twac + buf.append(&mut twac.serialize()); + + // confidence_interval + buf.extend_from_slice(&confidence_interval.to_be_bytes()[..]); + + // status + buf.push(status.clone() as u8); + + // corp_act + buf.push(corp_act.clone() as u8); + + // timestamp + buf.extend_from_slice(×tamp.to_be_bytes()[..]); + + buf + } +} + +/// Deserializes Price from raw bytes, sanity-check. +fn parse_pyth_price(price_data: &[u8]) -> SoliResult<&Price> { + if price_data.len() != mem::size_of::() { + trace!(&format!( + "parse_pyth_price: buffer length mismatch ({} expected, got {})", + mem::size_of::(), + price_data.len() + )); + return Err(ProgramError::InvalidAccountData.into()); + } + let price_account = pyth_client::cast::(price_data); + + if price_account.atype != AccountType::Price as u32 { + trace!(&format!( + "parse_pyth_price: AccountType mismatch ({} expected, got {})", + mem::size_of::(), + price_data.len() + )); + return Err(ProgramError::InvalidAccountData.into()); + } + + Ok(price_account) +} + +#[cfg(test)] +mod tests { + use super::*; + use pyth_client::{ + AccKey, + AccountType, + PriceComp, + PriceInfo, + }; + + macro_rules! empty_acckey { + () => { + AccKey { val: [0u8; 32] } + }; + } + + macro_rules! empty_priceinfo { + () => { + PriceInfo { + price: 0, + conf: 0, + status: PriceStatus::Unknown, + corp_act: CorpAction::NoCorpAct, + pub_slot: 0, + } + }; + } + + macro_rules! empty_pricecomp { + () => { + PriceComp { + publisher: empty_acckey!(), + agg: empty_priceinfo!(), + latest: empty_priceinfo!(), + } + }; + } + + macro_rules! empty_ema { + () => { + (&P2WEma::default()).into() + }; + } + + macro_rules! empty_price { + () => { + Price { + magic: pyth_client::MAGIC, + ver: pyth_client::VERSION, + atype: AccountType::Price as u32, + size: 0, + ptype: PriceType::Price, + expo: 0, + num: 0, + num_qt: 0, + last_slot: 0, + valid_slot: 0, + drv1: 0, + drv2: 0, + drv3: 0, + twap: empty_ema!(), + twac: empty_ema!(), + prod: empty_acckey!(), + next: empty_acckey!(), + prev_slot: 0, // valid slot of previous update + prev_price: 0, // aggregate price of previous update + prev_conf: 0, // confidence interval of previous update + agg: empty_priceinfo!(), + // A nice macro might come in handy if this gets annoying + comp: [ + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + empty_pricecomp!(), + ], + } + }; + } + + #[test] + fn test_parse_pyth_price_wrong_size_slices() { + assert!(parse_pyth_price(&[]).is_err()); + assert!(parse_pyth_price(vec![0u8; 1].as_slice()).is_err()); + } + + #[test] + fn test_normal_values() -> SoliResult<()> { + let price = Price { + expo: 5, + agg: PriceInfo { + price: 42, + ..empty_priceinfo!() + }, + ..empty_price!() + }; + let price_vec = vec![price]; + + // use the C repr to mock pyth's format + let (_, bytes, _) = unsafe { price_vec.as_slice().align_to::() }; + + parse_pyth_price(bytes)?; + Ok(()) + } + + #[test] + fn test_serialize() -> SoliResult<()> { + let product_id_bytes = [21u8; 32]; + let price_id_bytes = [222u8; 32]; + println!("Hex product_id: {:02X?}", &product_id_bytes); + println!("Hex price_id: {:02X?}", &price_id_bytes); + let attestation: PriceAttestation = PriceAttestation { + product_id: Pubkey::new_from_array(product_id_bytes), + price_id: Pubkey::new_from_array(price_id_bytes), + price: (0xdeadbeefdeadbabe as u64) as i64, + price_type: P2WPriceType::Price, + twap: P2WEma { + val: -42, + numer: 15, + denom: 37, + }, + twac: P2WEma { + val: 42, + numer: 1111, + denom: 2222, + }, + expo: -3, + status: P2WPriceStatus::Trading, + confidence_interval: 101, + corp_act: P2WCorpAction::NoCorpAct, + timestamp: 123456789i64, + }; + + println!("Regular: {:#?}", &attestation); + println!("Hex: {:#02X?}", &attestation); + println!("Hex Bytes: {:02X?}", attestation.serialize()); + Ok(()) + } +} diff --git a/solana/pyth2wormhole/program/src/types/pyth_extensions.rs b/solana/pyth2wormhole/program/src/types/pyth_extensions.rs new file mode 100644 index 00000000..75192970 --- /dev/null +++ b/solana/pyth2wormhole/program/src/types/pyth_extensions.rs @@ -0,0 +1,139 @@ +//! This module contains 1:1 (or close) copies of selected Pyth types +//! with quick and dirty enhancements. + +use std::mem; + +use pyth_client::{ + CorpAction, + Ema, + PriceStatus, + PriceType, +}; + +/// 1:1 Copy of pyth_client::PriceType with derived additional traits. +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum P2WPriceType { + Unknown, + Price, +} + +impl From<&PriceType> for P2WPriceType { + fn from(pt: &PriceType) -> Self { + match pt { + PriceType::Unknown => Self::Unknown, + PriceType::Price => Self::Price, + } + } +} + +impl Default for P2WPriceType { + fn default() -> Self { + Self::Price + } +} + +/// 1:1 Copy of pyth_client::PriceStatus with derived additional traits. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum P2WPriceStatus { + Unknown, + Trading, + Halted, + Auction, +} + +impl From<&PriceStatus> for P2WPriceStatus { + fn from(ps: &PriceStatus) -> Self { + match ps { + PriceStatus::Unknown => Self::Unknown, + PriceStatus::Trading => Self::Trading, + PriceStatus::Halted => Self::Halted, + PriceStatus::Auction => Self::Auction, + } + } +} + +impl Default for P2WPriceStatus { + fn default() -> Self { + Self::Trading + } +} + +/// 1:1 Copy of pyth_client::CorpAction with derived additional traits. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum P2WCorpAction { + NoCorpAct, +} + +impl Default for P2WCorpAction { + fn default() -> Self { + Self::NoCorpAct + } +} + +impl From<&CorpAction> for P2WCorpAction { + fn from(ca: &CorpAction) -> Self { + match ca { + CorpAction::NoCorpAct => P2WCorpAction::NoCorpAct, + } + } +} + +/// 1:1 Copy of pyth_client::Ema with all-pub fields. +#[derive(Clone, Default, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct P2WEma { + pub val: i64, + pub numer: i64, + pub denom: i64, +} + +/// CAUTION: This impl may panic and requires an unsafe cast +impl From<&Ema> for P2WEma { + fn from(ema: &Ema) -> Self { + let our_size = mem::size_of::(); + let upstream_size = mem::size_of::(); + if our_size == upstream_size { + unsafe { std::mem::transmute_copy(ema) } + } else { + dbg!(our_size); + dbg!(upstream_size); + // Because of private upstream fields it's impossible to + // complain about type-level changes at compile-time + panic!("P2WEma sizeof mismatch") + } + } +} + +/// CAUTION: This impl may panic and requires an unsafe cast +impl Into for &P2WEma { + fn into(self) -> Ema { + let our_size = mem::size_of::(); + let upstream_size = mem::size_of::(); + if our_size == upstream_size { + unsafe { std::mem::transmute_copy(self) } + } else { + dbg!(our_size); + dbg!(upstream_size); + // Because of private upstream fields it's impossible to + // complain about type-level changes at compile-time + panic!("P2WEma sizeof mismatch") + } + } +} + +impl P2WEma { + pub fn serialize(&self) -> Vec { + let mut v = vec![]; + // val + v.extend(&self.val.to_be_bytes()[..]); + + // numer + v.extend(&self.numer.to_be_bytes()[..]); + + // denom + v.extend(&self.denom.to_be_bytes()[..]); + + v + } +} diff --git a/solana/solitaire/program/src/lib.rs b/solana/solitaire/program/src/lib.rs index f3a72461..8e302ecd 100644 --- a/solana/solitaire/program/src/lib.rs +++ b/solana/solitaire/program/src/lib.rs @@ -72,10 +72,12 @@ pub use crate::{ peel::Peel, persist::Persist, seeded::{ + invoke_seeded, AccountOwner, AccountSize, Creatable, Owned, + Seeded, }, }, types::*, diff --git a/solana/solitaire/rocksalt/src/lib.rs b/solana/solitaire/rocksalt/src/lib.rs index d178e9fe..9b0f60c4 100644 --- a/solana/solitaire/rocksalt/src/lib.rs +++ b/solana/solitaire/rocksalt/src/lib.rs @@ -118,7 +118,7 @@ pub fn derive_from_accounts(input: TokenStream) -> TokenStream { } impl #combined_impl_g solitaire::Peel<'a, 'b, 'c> for #name #type_g { - fn peel(ctx: &'c mut Context<'a, 'b, 'c, I>) -> solitaire::Result where Self: Sized { + fn peel(ctx: &'c mut solitaire::Context<'a, 'b, 'c, I>) -> solitaire::Result where Self: Sized { let v: #name #type_g = FromAccounts::from(ctx.this, ctx.iter, ctx.data)?; Ok(v) }