From a8465ef7914ddc7cdfb3a8693e90469e22ae5f2d Mon Sep 17 00:00:00 2001 From: Stan Drozd Date: Mon, 4 Oct 2021 15:49:43 +0200 Subject: [PATCH] pyth2wormhole: implement deserialization, print sequence Change-Id: I39308ad6431df52f35a0496e71836497561640c5 --- solana/pyth2wormhole/Cargo.lock | 4 + solana/pyth2wormhole/client/Cargo.toml | 1 + solana/pyth2wormhole/client/src/main.rs | 51 +++-- solana/pyth2wormhole/program/Cargo.toml | 8 +- solana/pyth2wormhole/program/src/attest.rs | 8 +- solana/pyth2wormhole/program/src/types/mod.rs | 178 ++++++++++++++++-- .../program/src/types/pyth_extensions.rs | 25 ++- solana/pyth2wormhole/program/src/wasm.rs | 21 +++ 8 files changed, 257 insertions(+), 39 deletions(-) diff --git a/solana/pyth2wormhole/Cargo.lock b/solana/pyth2wormhole/Cargo.lock index 49959ed1..aa19a190 100644 --- a/solana/pyth2wormhole/Cargo.lock +++ b/solana/pyth2wormhole/Cargo.lock @@ -2067,6 +2067,9 @@ dependencies = [ "bridge", "pyth-client", "rocksalt", + "serde", + "serde_derive", + "serde_json", "solana-program", "solitaire", "solitaire-client", @@ -2087,6 +2090,7 @@ dependencies = [ "solana-client", "solana-program", "solana-sdk", + "solana-transaction-status", "solitaire", "solitaire-client", ] diff --git a/solana/pyth2wormhole/client/Cargo.toml b/solana/pyth2wormhole/client/Cargo.toml index 004a57de..20e3bf2e 100644 --- a/solana/pyth2wormhole/client/Cargo.toml +++ b/solana/pyth2wormhole/client/Cargo.toml @@ -19,5 +19,6 @@ shellexpand = "2.1.0" solana-client = "=1.7.0" solana-program = "=1.7.0" solana-sdk = "=1.7.0" +solana-transaction-status = "=1.7.0" solitaire-client = {path = "../../solitaire/client"} solitaire = {path = "../../solitaire/program"} diff --git a/solana/pyth2wormhole/client/src/main.rs b/solana/pyth2wormhole/client/src/main.rs index d78228d9..5b3c7199 100644 --- a/solana/pyth2wormhole/client/src/main.rs +++ b/solana/pyth2wormhole/client/src/main.rs @@ -1,8 +1,14 @@ pub mod cli; -use borsh::{BorshDeserialize, BorshSerialize}; +use borsh::{ + BorshDeserialize, + BorshSerialize, +}; use clap::Clap; -use log::{LevelFilter, error}; +use log::{ + warn, + LevelFilter, +}; use solana_client::rpc_client::RpcClient; use solana_program::{ hash::Hash, @@ -22,6 +28,7 @@ use solana_sdk::{ signature::read_keypair_file, transaction::Transaction, }; +use solana_transaction_status::UiTransactionEncoding; use solitaire::{ processors::seeded::Seeded, AccountState, @@ -52,6 +59,7 @@ use bridge::{ }; use pyth2wormhole::{ + attest::P2WEmitter, config::P2WConfigAccount, initialize::InitializeAccounts, set_config::SetConfigAccounts, @@ -62,6 +70,8 @@ use pyth2wormhole::{ pub type ErrBox = Box; +pub const SEQNO_PREFIX: &'static str = "Program log: Sequence: "; + fn main() -> Result<(), ErrBox> { let cli = Cli::parse(); init_logging(cli.log_level); @@ -87,7 +97,7 @@ fn main() -> Result<(), ErrBox> { recent_blockhash, )?, Action::SetConfig { - owner, + ref owner, new_owner_addr, new_wh_prog, new_pyth_owner_addr, @@ -115,7 +125,23 @@ fn main() -> Result<(), ErrBox> { )?, }; - rpc_client.send_and_confirm_transaction_with_spinner(&tx)?; + let sig = rpc_client.send_and_confirm_transaction_with_spinner(&tx)?; + + // To complete attestation, retrieve sequence number from transaction logs + if let Action::Attest { .. } = cli.action { + let this_tx = rpc_client.get_transaction(&sig, UiTransactionEncoding::Json)?; + + if let Some(logs) = this_tx.transaction.meta.and_then(|meta| meta.log_messages) { + for log in logs { + if log.starts_with(SEQNO_PREFIX) { + let seqno = log.replace(SEQNO_PREFIX, ""); + println!("Sequence number: {}", seqno); + } + } + } else { + warn!("Could not get program logs for attestation"); + } + } Ok(()) } @@ -203,17 +229,19 @@ fn handle_attest( nonce: u32, recent_blockhash: Hash, ) -> Result { - let emitter_keypair = Keypair::new(); let message_keypair = Keypair::new(); + let emitter_addr = P2WEmitter::key(None, &p2w_addr); + let p2w_config_addr = P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_addr); - let config = Pyth2WormholeConfig::try_from_slice(rpc.get_account_data(&p2w_config_addr)?.as_slice())?; + let config = + Pyth2WormholeConfig::try_from_slice(rpc.get_account_data(&p2w_config_addr)?.as_slice())?; // Derive dynamic seeded accounts let seq_addr = Sequence::key( &SequenceDerivationData { - emitter_key: &emitter_keypair.pubkey(), + emitter_key: &emitter_addr, }, &config.wh_prog, ); @@ -225,10 +253,7 @@ fn handle_attest( // system_program AccountMeta::new_readonly(system_program::id(), false), // config - AccountMeta::new_readonly( - p2w_config_addr, - false, - ), + AccountMeta::new_readonly(p2w_config_addr, false), // pyth_product AccountMeta::new_readonly(product_addr, false), // pyth_price @@ -245,7 +270,7 @@ fn handle_attest( // wh_message AccountMeta::new(message_keypair.pubkey(), true), // wh_emitter - AccountMeta::new_readonly(emitter_keypair.pubkey(), true), + AccountMeta::new_readonly(emitter_addr, false), // wh_sequence AccountMeta::new(seq_addr, false), // wh_fee_collector @@ -264,7 +289,7 @@ fn handle_attest( let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas); // Signers that use off-chain keypairs - let signer_keypairs = vec![&payer, &message_keypair, &emitter_keypair]; + let signer_keypairs = vec![&payer, &message_keypair]; let tx_signed = Transaction::new_signed_with_payer::>( &[ix], diff --git a/solana/pyth2wormhole/program/Cargo.toml b/solana/pyth2wormhole/program/Cargo.toml index 560b056d..55b65d6e 100644 --- a/solana/pyth2wormhole/program/Cargo.toml +++ b/solana/pyth2wormhole/program/Cargo.toml @@ -13,7 +13,7 @@ default = ["bridge/no-entrypoint"] client = ["solitaire/client", "solitaire-client", "no-entrypoint"] trace = ["solitaire/trace", "bridge/trace"] no-entrypoint = [] -wasm = ["no-entrypoint", "wasm-bindgen"] +wasm = ["no-entrypoint", "wasm-bindgen", "serde", "serde_derive", "serde_json"] [dependencies] bridge = {path = "../../bridge/program"} @@ -24,4 +24,8 @@ 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"} -wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true} \ No newline at end of file +# Crates needed for easier wasm data passing +wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true} +serde = { version = "1", optional = true} +serde_derive = { version = "1", optional = true} +serde_json = { version = "1", optional = true} \ No newline at end of file diff --git a/solana/pyth2wormhole/program/src/attest.rs b/solana/pyth2wormhole/program/src/attest.rs index c0ffb9a6..d2ae326a 100644 --- a/solana/pyth2wormhole/program/src/attest.rs +++ b/solana/pyth2wormhole/program/src/attest.rs @@ -40,12 +40,15 @@ use solitaire::{ Peel, Result as SoliResult, Seeded, + invoke_seeded, Signer, SolitaireError, Sysvar, ToInstruction, }; +pub type P2WEmitter<'b> = Derive, "p2w-emitter">; + #[derive(FromAccounts, ToInstruction)] pub struct Attest<'b> { // Payer also used for wormhole @@ -67,7 +70,7 @@ pub struct Attest<'b> { pub wh_message: Signer>>, /// Emitter of the VAA - pub wh_emitter: Info<'b>, + pub wh_emitter: P2WEmitter<'b>, /// Tracker for the emitter sequence pub wh_sequence: Mut>, @@ -163,7 +166,8 @@ pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> So ); trace!("Before cross-call"); - invoke(&ix, ctx.accounts)?; + + invoke_seeded(&ix, ctx, &accs.wh_emitter, None)?; Ok(()) } diff --git a/solana/pyth2wormhole/program/src/types/mod.rs b/solana/pyth2wormhole/program/src/types/mod.rs index 90aab032..c7a405b3 100644 --- a/solana/pyth2wormhole/program/src/types/mod.rs +++ b/solana/pyth2wormhole/program/src/types/mod.rs @@ -1,6 +1,13 @@ pub mod pyth_extensions; -use std::mem; +use std::{ + convert::{ + TryFrom, + TryInto, + }, + io::Read, + mem, +}; use borsh::BorshSerialize; use pyth_client::{ @@ -11,9 +18,14 @@ use pyth_client::{ PriceStatus, PriceType, }; -use solana_program::{clock::UnixTimestamp, program_error::ProgramError, pubkey::Pubkey}; +use solana_program::{ + clock::UnixTimestamp, + program_error::ProgramError, + pubkey::Pubkey, +}; use solitaire::{ trace, + ErrBox, Result as SoliResult, SolitaireError, }; @@ -33,6 +45,8 @@ pub const P2W_MAGIC: &'static [u8] = b"P2WH"; /// Format version used and understood by this codebase pub const P2W_FORMAT_VERSION: u16 = 1; +pub const PUBKEY_LEN: usize = 32; + /// Decides the format of following bytes #[repr(u8)] pub enum PayloadId { @@ -42,6 +56,7 @@ pub enum PayloadId { // On-chain data types #[derive(Clone, Default, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))] pub struct PriceAttestation { pub product_id: Pubkey, pub price_id: Pubkey, @@ -57,7 +72,11 @@ pub struct PriceAttestation { } impl PriceAttestation { - pub fn from_pyth_price_bytes(price_id: Pubkey, timestamp: UnixTimestamp, value: &[u8]) -> Result { + pub fn from_pyth_price_bytes( + price_id: Pubkey, + timestamp: UnixTimestamp, + value: &[u8], + ) -> Result { let price = parse_pyth_price(value)?; Ok(PriceAttestation { @@ -71,14 +90,14 @@ impl PriceAttestation { confidence_interval: price.agg.conf, status: (&price.agg.status).into(), corp_act: (&price.agg.corp_act).into(), - timestamp: timestamp, + 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)] + #[deny(warnings)] let PriceAttestation { product_id, price_id, @@ -90,7 +109,7 @@ impl PriceAttestation { confidence_interval, status, corp_act, - timestamp + timestamp, } = self; // magic @@ -123,20 +142,136 @@ impl PriceAttestation { // twac buf.append(&mut twac.serialize()); - // confidence_interval - buf.extend_from_slice(&confidence_interval.to_be_bytes()[..]); + // confidence_interval + buf.extend_from_slice(&confidence_interval.to_be_bytes()[..]); - // status - buf.push(status.clone() as u8); + // status + buf.push(status.clone() as u8); - // corp_act - buf.push(corp_act.clone() as u8); + // corp_act + buf.push(corp_act.clone() as u8); - // timestamp - buf.extend_from_slice(×tamp.to_be_bytes()[..]); + // timestamp + buf.extend_from_slice(×tamp.to_be_bytes()[..]); buf } + pub fn deserialize(mut bytes: impl Read) -> Result { + use P2WCorpAction::*; + use P2WPriceStatus::*; + use P2WPriceType::*; + + println!("Using {} bytes for magic", P2W_MAGIC.len()); + let mut magic_vec = vec![0u8; P2W_MAGIC.len()]; + + bytes.read_exact(magic_vec.as_mut_slice())?; + + if magic_vec.as_slice() != P2W_MAGIC { + return Err(format!( + "Invalid magic {:02X?}, expected {:02X?}", + magic_vec, P2W_MAGIC, + ) + .into()); + } + + let mut version_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_VERSION)]; + bytes.read_exact(version_vec.as_mut_slice())?; + let mut version = u16::from_be_bytes(version_vec.as_slice().try_into()?); + + if version != P2W_FORMAT_VERSION { + return Err(format!( + "Unsupported format version {}, expected {}", + version, P2W_FORMAT_VERSION + ) + .into()); + } + + let mut payload_id_vec = vec![0u8; mem::size_of::()]; + bytes.read_exact(payload_id_vec.as_mut_slice())?; + + if PayloadId::PriceAttestation as u8 != payload_id_vec[0] { + return Err(format!( + "Invalid Payload ID {}, expected {}", + payload_id_vec[0], + PayloadId::PriceAttestation as u8, + ) + .into()); + } + + let mut product_id_vec = vec![0u8; PUBKEY_LEN]; + bytes.read_exact(product_id_vec.as_mut_slice())?; + let product_id = Pubkey::new(product_id_vec.as_slice()); + + let mut price_id_vec = vec![0u8; PUBKEY_LEN]; + bytes.read_exact(price_id_vec.as_mut_slice())?; + let price_id = Pubkey::new(price_id_vec.as_slice()); + + let mut price_type_vec = vec![0u8; mem::size_of::()]; + bytes.read_exact(price_type_vec.as_mut_slice())?; + let price_type = match price_type_vec[0] { + a if a == Price as u8 => Price, + a if a == P2WPriceType::Unknown as u8 => P2WPriceType::Unknown, + other => { + return Err(format!("Invalid price_type value {}", other).into()); + } + }; + + let mut price_vec = vec![0u8; mem::size_of::()]; + bytes.read_exact(price_vec.as_mut_slice())?; + let price = i64::from_be_bytes(price_vec.as_slice().try_into()?); + + let mut expo_vec = vec![0u8; mem::size_of::()]; + bytes.read_exact(expo_vec.as_mut_slice())?; + let expo = i32::from_be_bytes(expo_vec.as_slice().try_into()?); + + let twap = P2WEma::deserialize(&mut bytes)?; + let twac = P2WEma::deserialize(&mut bytes)?; + + println!("twac OK"); + let mut confidence_interval_vec = vec![0u8; mem::size_of::()]; + bytes.read_exact(confidence_interval_vec.as_mut_slice())?; + let confidence_interval = u64::from_be_bytes(confidence_interval_vec.as_slice().try_into()?); + + let mut status_vec = vec![0u8; mem::size_of::()]; + bytes.read_exact(status_vec.as_mut_slice())?; + let status = match status_vec[0] { + a if a == P2WPriceStatus::Unknown as u8 => P2WPriceStatus::Unknown, + a if a == Trading as u8 => Trading, + a if a == Halted as u8 => Halted, + a if a == Auction as u8 => Auction, + other => { + return Err(format!("Invalid status value {}", other).into()); + } + }; + + + let mut corp_act_vec = vec![0u8; mem::size_of::()]; + bytes.read_exact(corp_act_vec.as_mut_slice())?; + let corp_act = match corp_act_vec[0] { + a if a == NoCorpAct as u8 => NoCorpAct, + other => { + return Err(format!("Invalid corp_act value {}", other).into()); + } + }; + + let mut timestamp_vec = vec![0u8; mem::size_of::()]; + bytes.read_exact(timestamp_vec.as_mut_slice())?; + let timestamp = UnixTimestamp::from_be_bytes(timestamp_vec.as_slice().try_into()?); + + Ok( Self { + product_id, + price_id, + price_type, + price, + expo, + twap, + twac, + confidence_interval, + status, + corp_act, + timestamp + }) + } } /// Deserializes Price from raw bytes, sanity-check. @@ -296,7 +431,7 @@ mod tests { } #[test] - fn test_serialize() -> SoliResult<()> { + fn test_serialize_deserialize() -> Result<(), ErrBox> { let product_id_bytes = [21u8; 32]; let price_id_bytes = [222u8; 32]; println!("Hex product_id: {:02X?}", &product_id_bytes); @@ -305,7 +440,7 @@ mod tests { 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, + price_type: P2WPriceType::Price, twap: P2WEma { val: -42, numer: 15, @@ -317,15 +452,18 @@ mod tests { denom: 2222, }, expo: -3, - status: P2WPriceStatus::Trading, + status: P2WPriceStatus::Trading, confidence_interval: 101, - corp_act: P2WCorpAction::NoCorpAct, - timestamp: 123456789i64, + corp_act: P2WCorpAction::NoCorpAct, + timestamp: 123456789i64, }; println!("Regular: {:#?}", &attestation); println!("Hex: {:#02X?}", &attestation); - println!("Hex Bytes: {:02X?}", attestation.serialize()); + let bytes = attestation.serialize(); + println!("Hex Bytes: {:02X?}", bytes); + + assert_eq!(PriceAttestation::deserialize(bytes.as_slice())?, attestation); Ok(()) } } diff --git a/solana/pyth2wormhole/program/src/types/pyth_extensions.rs b/solana/pyth2wormhole/program/src/types/pyth_extensions.rs index 75192970..ee056403 100644 --- a/solana/pyth2wormhole/program/src/types/pyth_extensions.rs +++ b/solana/pyth2wormhole/program/src/types/pyth_extensions.rs @@ -1,7 +1,7 @@ //! This module contains 1:1 (or close) copies of selected Pyth types //! with quick and dirty enhancements. -use std::mem; +use std::{convert::TryInto, io::Read, mem}; use pyth_client::{ CorpAction, @@ -9,9 +9,11 @@ use pyth_client::{ PriceStatus, PriceType, }; +use solitaire::ErrBox; /// 1:1 Copy of pyth_client::PriceType with derived additional traits. #[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))] #[repr(u8)] pub enum P2WPriceType { Unknown, @@ -35,6 +37,7 @@ impl Default for P2WPriceType { /// 1:1 Copy of pyth_client::PriceStatus with derived additional traits. #[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))] pub enum P2WPriceStatus { Unknown, Trading, @@ -55,12 +58,13 @@ impl From<&PriceStatus> for P2WPriceStatus { impl Default for P2WPriceStatus { fn default() -> Self { - Self::Trading + Self::Trading } } /// 1:1 Copy of pyth_client::CorpAction with derived additional traits. #[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))] pub enum P2WCorpAction { NoCorpAct, } @@ -81,6 +85,7 @@ impl From<&CorpAction> for P2WCorpAction { /// 1:1 Copy of pyth_client::Ema with all-pub fields. #[derive(Clone, Default, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "wasm", derive(serde_derive::Serialize, serde_derive::Deserialize))] #[repr(C)] pub struct P2WEma { pub val: i64, @@ -136,4 +141,20 @@ impl P2WEma { v } + + pub fn deserialize(mut bytes: impl Read) -> Result { + let mut val_vec = vec![0u8; mem::size_of::()]; + bytes.read_exact(val_vec.as_mut_slice())?; + let val = i64::from_be_bytes(val_vec.as_slice().try_into()?); + + let mut numer_vec = vec![0u8; mem::size_of::()]; + bytes.read_exact(numer_vec.as_mut_slice())?; + let numer = i64::from_be_bytes(numer_vec.as_slice().try_into()?); + + let mut denom_vec = vec![0u8; mem::size_of::()]; + bytes.read_exact(denom_vec.as_mut_slice())?; + let denom = i64::from_be_bytes(denom_vec.as_slice().try_into()?); + + Ok(Self { val, numer, denom }) + } } diff --git a/solana/pyth2wormhole/program/src/wasm.rs b/solana/pyth2wormhole/program/src/wasm.rs index 675f4d12..dc9a6361 100644 --- a/solana/pyth2wormhole/program/src/wasm.rs +++ b/solana/pyth2wormhole/program/src/wasm.rs @@ -1,8 +1,29 @@ +use solitaire::Seeded; +use solana_program::pubkey::Pubkey; use wasm_bindgen::prelude::*; +use std::str::FromStr; + +use crate::{attest::P2WEmitter, types::PriceAttestation}; + /// sanity check for wasm compilation, TODO(sdrozd): remove after /// meaningful endpoints are added #[wasm_bindgen] pub fn hello_p2w() -> String { "Ciao mondo!".to_owned() } + +#[wasm_bindgen] +pub fn get_emitter_address(program_id: String) -> Vec { + let program_id = Pubkey::from_str(program_id.as_str()).unwrap(); + let emitter = P2WEmitter::key(None, &program_id); + + emitter.to_bytes().to_vec() +} + +#[wasm_bindgen] +pub fn parse_attestation(bytes: Vec) -> JsValue { + let a = PriceAttestation::deserialize(bytes.as_slice()).unwrap(); + + JsValue::from_serde(&a).unwrap() +}