//! Constants and values common to every p2w custom-serialized message. //! //! The format makes no attempt to provide human-readable symbol names //! in favor of explicit product/price Solana account addresses //! (IDs). This choice was made to disambiguate any symbols with //! similar human-readable names and provide a failsafe for some of //! the probable adversarial scenarios. pub use pyth_sdk::{ Identifier, PriceStatus, UnixTimestamp, }; #[cfg(feature = "solana")] use { pyth_sdk_solana::state::PriceAccount, solitaire::{ Derive, Info, }, }; use { serde::{ Deserialize, Serialize, Serializer, }, std::{ borrow::Borrow, convert::TryInto, io::Read, iter::Iterator, mem, }, }; pub type ErrBox = Box; /// Precedes every message implementing the p2w serialization format pub const P2W_MAGIC: &[u8] = b"P2WH"; /// Format version used and understood by this codebase pub const P2W_FORMAT_VER_MAJOR: u16 = 3; /// Starting with v3, format introduces a minor version to mark /// forward-compatible iterations. /// IMPORTANT: Remember to reset this to 0 whenever major version is /// bumped. /// Changelog: /// * v3.1 - last_attested_publish_time field added pub const P2W_FORMAT_VER_MINOR: u16 = 1; /// Starting with v3, format introduces append-only /// forward-compatibility to the header. This is the current number of /// bytes after the hdr_size field. After the specified bytes, inner /// payload-specific fields begin. pub const P2W_FORMAT_HDR_SIZE: u16 = 1; pub const PUBKEY_LEN: usize = 32; /// Emmitter Address to wormhole is a PDA with seed p2w-emmiter from attestation contract #[cfg(feature = "solana")] pub type P2WEmitter<'b> = Derive, "p2w-emitter">; /// Decides the format of following bytes #[repr(u8)] pub enum PayloadId { PriceAttestation = 1, // Not in use, currently batch attestations imply PriceAttestation messages inside PriceBatchAttestation = 2, } /// The main attestation data type. /// /// Important: For maximum security, *both* product_id and price_id /// should be used as storage keys for known attestations in target /// chain logic. /// /// NOTE(2022-04-25): the serde attributes help prevent math errors, /// and no less annoying low-effort serialization override method is known. #[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PriceAttestation { #[serde(serialize_with = "pubkey_to_hex")] pub product_id: Identifier, #[serde(serialize_with = "pubkey_to_hex")] pub price_id: Identifier, #[serde(serialize_with = "use_to_string")] pub price: i64, #[serde(serialize_with = "use_to_string")] pub conf: u64, pub expo: i32, #[serde(serialize_with = "use_to_string")] pub ema_price: i64, #[serde(serialize_with = "use_to_string")] pub ema_conf: u64, pub status: PriceStatus, pub num_publishers: u32, pub max_num_publishers: u32, pub attestation_time: UnixTimestamp, pub publish_time: UnixTimestamp, pub prev_publish_time: UnixTimestamp, #[serde(serialize_with = "use_to_string")] pub prev_price: i64, #[serde(serialize_with = "use_to_string")] pub prev_conf: u64, pub last_attested_publish_time: UnixTimestamp, } /// Helper allowing ToString implementers to be serialized as strings accordingly pub fn use_to_string(val: &T, s: S) -> Result where T: ToString, S: Serializer, { s.serialize_str(&val.to_string()) } pub fn pubkey_to_hex(val: &Identifier, s: S) -> Result where S: Serializer, { s.serialize_str(&hex::encode(val.to_bytes())) } #[derive(Clone, Default, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct BatchPriceAttestation { pub price_attestations: Vec, } impl BatchPriceAttestation { /// Turn a bunch of attestations into a combined payload. /// /// Batches assume constant-size attestations within a single batch. pub fn serialize(&self) -> Result, ErrBox> { // magic let mut buf = P2W_MAGIC.to_vec(); // major_version buf.extend_from_slice(&P2W_FORMAT_VER_MAJOR.to_be_bytes()[..]); // minor_version buf.extend_from_slice(&P2W_FORMAT_VER_MINOR.to_be_bytes()[..]); // hdr_size buf.extend_from_slice(&P2W_FORMAT_HDR_SIZE.to_be_bytes()[..]); // payload_id buf.push(PayloadId::PriceBatchAttestation as u8); // Header is over. NOTE: If you need to append to the header, // make sure that the number of bytes after hdr_size is // reflected in the P2W_FORMAT_HDR_SIZE constant. // n_attestations buf.extend_from_slice(&(self.price_attestations.len() as u16).to_be_bytes()[..]); let mut attestation_size = 0; // Will be determined as we serialize attestations let mut serialized_attestations = Vec::with_capacity(self.price_attestations.len()); for (idx, a) in self.price_attestations.iter().enumerate() { // Learn the current attestation's size let serialized = PriceAttestation::serialize(a.borrow()); let a_len = serialized.len(); // Verify it's the same as the first one we saw for the batch, assign if we're first. if attestation_size > 0 { if a_len != attestation_size { return Err(format!( "attestation {} serializes to {} bytes, {} expected", idx + 1, a_len, attestation_size ) .into()); } } else { attestation_size = a_len; } serialized_attestations.push(serialized); } // attestation_size buf.extend_from_slice(&(attestation_size as u16).to_be_bytes()[..]); for mut s in serialized_attestations.into_iter() { buf.append(&mut s) } Ok(buf) } pub fn deserialize(mut bytes: impl Read) -> Result { 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 {magic_vec:02X?}, expected {P2W_MAGIC:02X?}",).into(), ); } let mut major_version_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_VER_MAJOR)]; bytes.read_exact(major_version_vec.as_mut_slice())?; let major_version = u16::from_be_bytes(major_version_vec.as_slice().try_into()?); // Major must match exactly if major_version != P2W_FORMAT_VER_MAJOR { return Err(format!( "Unsupported format major_version {major_version}, expected {P2W_FORMAT_VER_MAJOR}" ) .into()); } let mut minor_version_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_VER_MINOR)]; bytes.read_exact(minor_version_vec.as_mut_slice())?; let minor_version = u16::from_be_bytes(minor_version_vec.as_slice().try_into()?); // Only older minors are not okay for this codebase if minor_version < P2W_FORMAT_VER_MINOR { return Err(format!( "Unsupported format minor_version {minor_version}, expected {P2W_FORMAT_VER_MINOR} or more" ) .into()); } // Read header size value let mut hdr_size_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_HDR_SIZE)]; bytes.read_exact(hdr_size_vec.as_mut_slice())?; let hdr_size = u16::from_be_bytes(hdr_size_vec.as_slice().try_into()?); // Consume the declared number of remaining header // bytes. Remaining header fields must be read from hdr_buf let mut hdr_buf = vec![0u8; hdr_size as usize]; bytes.read_exact(hdr_buf.as_mut_slice())?; let mut payload_id_vec = vec![0u8; mem::size_of::()]; hdr_buf .as_slice() .read_exact(payload_id_vec.as_mut_slice())?; if payload_id_vec[0] != PayloadId::PriceBatchAttestation as u8 { return Err(format!( "Invalid Payload ID {}, expected {}", payload_id_vec[0], PayloadId::PriceBatchAttestation as u8, ) .into()); } // Header consumed, continue with remaining fields let mut batch_len_vec = vec![0u8; 2]; bytes.read_exact(batch_len_vec.as_mut_slice())?; let batch_len = u16::from_be_bytes(batch_len_vec.as_slice().try_into()?); let mut attestation_size_vec = vec![0u8; 2]; bytes.read_exact(attestation_size_vec.as_mut_slice())?; let attestation_size = u16::from_be_bytes(attestation_size_vec.as_slice().try_into()?); let mut ret = Vec::with_capacity(batch_len as usize); for i in 0..batch_len { let mut attestation_buf = vec![0u8; attestation_size as usize]; bytes.read_exact(attestation_buf.as_mut_slice())?; match PriceAttestation::deserialize(attestation_buf.as_slice()) { Ok(attestation) => ret.push(attestation), Err(e) => { return Err(format!("PriceAttestation {}/{}: {}", i + 1, batch_len, e).into()) } } } Ok(Self { price_attestations: ret, }) } } // On-chain data types impl PriceAttestation { #[cfg(feature = "solana")] pub fn from_pyth_price_bytes( price_id: Identifier, attestation_time: UnixTimestamp, last_attested_publish_time: UnixTimestamp, value: &[u8], ) -> Result { let price_struct = pyth_sdk_solana::state::load_price_account(value)?; Ok(Self::from_pyth_price_struct( price_id, attestation_time, last_attested_publish_time, price_struct, )) } #[cfg(feature = "solana")] pub fn from_pyth_price_struct( price_id: Identifier, attestation_time: UnixTimestamp, last_attested_publish_time: UnixTimestamp, price: &PriceAccount, ) -> Self { PriceAttestation { product_id: Identifier::new(price.prod.val), price_id, price: price.agg.price, conf: price.agg.conf, expo: price.expo, ema_price: price.ema_price.val, ema_conf: price.ema_conf.val as u64, status: price.agg.status, num_publishers: price.num_qt, max_num_publishers: price.num, attestation_time, publish_time: price.timestamp, prev_publish_time: price.prev_timestamp, prev_price: price.prev_price, prev_conf: price.prev_conf, last_attested_publish_time, } } /// 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, conf, expo, ema_price, ema_conf, status, num_publishers, max_num_publishers, attestation_time, publish_time, prev_publish_time, prev_price, prev_conf, last_attested_publish_time, } = self; let mut buf = Vec::new(); // product_id buf.extend_from_slice(&product_id.to_bytes()[..]); // price_id buf.extend_from_slice(&price_id.to_bytes()[..]); // price buf.extend_from_slice(&price.to_be_bytes()[..]); // conf buf.extend_from_slice(&conf.to_be_bytes()[..]); // expo buf.extend_from_slice(&expo.to_be_bytes()[..]); // ema_price buf.extend_from_slice(&ema_price.to_be_bytes()[..]); // ema_conf buf.extend_from_slice(&ema_conf.to_be_bytes()[..]); // status buf.push(*status as u8); // num_publishers buf.extend_from_slice(&num_publishers.to_be_bytes()[..]); // max_num_publishers buf.extend_from_slice(&max_num_publishers.to_be_bytes()[..]); // attestation_time buf.extend_from_slice(&attestation_time.to_be_bytes()[..]); // publish_time buf.extend_from_slice(&publish_time.to_be_bytes()[..]); // prev_publish_time buf.extend_from_slice(&prev_publish_time.to_be_bytes()[..]); // prev_price buf.extend_from_slice(&prev_price.to_be_bytes()[..]); // prev_conf buf.extend_from_slice(&prev_conf.to_be_bytes()[..]); // last_attested_publish_time buf.extend_from_slice(&last_attested_publish_time.to_be_bytes()[..]); buf } pub fn deserialize(mut bytes: impl Read) -> Result { let mut product_id_vec = vec![0u8; PUBKEY_LEN]; bytes.read_exact(product_id_vec.as_mut_slice())?; let product_id = Identifier::new(product_id_vec.as_slice().try_into()?); let mut price_id_vec = vec![0u8; PUBKEY_LEN]; bytes.read_exact(price_id_vec.as_mut_slice())?; let price_id = Identifier::new(price_id_vec.as_slice().try_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 conf_vec = vec![0u8; mem::size_of::()]; bytes.read_exact(conf_vec.as_mut_slice())?; let conf = u64::from_be_bytes(conf_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 mut ema_price_vec = vec![0u8; mem::size_of::()]; bytes.read_exact(ema_price_vec.as_mut_slice())?; let ema_price = i64::from_be_bytes(ema_price_vec.as_slice().try_into()?); let mut ema_conf_vec = vec![0u8; mem::size_of::()]; bytes.read_exact(ema_conf_vec.as_mut_slice())?; let ema_conf = u64::from_be_bytes(ema_conf_vec.as_slice().try_into()?); let mut status_vec = vec![0u8]; bytes.read_exact(status_vec.as_mut_slice())?; let status = match status_vec[0] { a if a == PriceStatus::Unknown as u8 => PriceStatus::Unknown, a if a == PriceStatus::Trading as u8 => PriceStatus::Trading, a if a == PriceStatus::Halted as u8 => PriceStatus::Halted, a if a == PriceStatus::Auction as u8 => PriceStatus::Auction, other => { return Err(format!("Invalid status value {other}").into()); } }; let mut num_publishers_vec = vec![0u8; mem::size_of::()]; bytes.read_exact(num_publishers_vec.as_mut_slice())?; let num_publishers = u32::from_be_bytes(num_publishers_vec.as_slice().try_into()?); let mut max_num_publishers_vec = vec![0u8; mem::size_of::()]; bytes.read_exact(max_num_publishers_vec.as_mut_slice())?; let max_num_publishers = u32::from_be_bytes(max_num_publishers_vec.as_slice().try_into()?); let mut attestation_time_vec = vec![0u8; mem::size_of::()]; bytes.read_exact(attestation_time_vec.as_mut_slice())?; let attestation_time = UnixTimestamp::from_be_bytes(attestation_time_vec.as_slice().try_into()?); let mut publish_time_vec = vec![0u8; mem::size_of::()]; bytes.read_exact(publish_time_vec.as_mut_slice())?; let publish_time = UnixTimestamp::from_be_bytes(publish_time_vec.as_slice().try_into()?); let mut prev_publish_time_vec = vec![0u8; mem::size_of::()]; bytes.read_exact(prev_publish_time_vec.as_mut_slice())?; let prev_publish_time = UnixTimestamp::from_be_bytes(prev_publish_time_vec.as_slice().try_into()?); let mut prev_price_vec = vec![0u8; mem::size_of::()]; bytes.read_exact(prev_price_vec.as_mut_slice())?; let prev_price = i64::from_be_bytes(prev_price_vec.as_slice().try_into()?); let mut prev_conf_vec = vec![0u8; mem::size_of::()]; bytes.read_exact(prev_conf_vec.as_mut_slice())?; let prev_conf = u64::from_be_bytes(prev_conf_vec.as_slice().try_into()?); let mut last_attested_publish_time_vec = vec![0u8; mem::size_of::()]; bytes.read_exact(last_attested_publish_time_vec.as_mut_slice())?; let last_attested_publish_time = UnixTimestamp::from_be_bytes(last_attested_publish_time_vec.as_slice().try_into()?); Ok(Self { product_id, price_id, price, conf, expo, ema_price, ema_conf, status, num_publishers, max_num_publishers, attestation_time, publish_time, prev_publish_time, prev_price, prev_conf, last_attested_publish_time, }) } } /// This test suite of the format doubles as a test payload generator; /// print statements help provide plausible serialized data on demand /// using `cargo test -- --nocapture`. #[cfg(test)] mod tests { use { super::*, pyth_sdk_solana::state::PriceStatus, }; fn mock_attestation(prod: Option<[u8; 32]>, price: Option<[u8; 32]>) -> PriceAttestation { let product_id_bytes = prod.unwrap_or([21u8; 32]); let price_id_bytes = price.unwrap_or([222u8; 32]); PriceAttestation { product_id: Identifier::new(product_id_bytes), price_id: Identifier::new(price_id_bytes), price: 0x2bad2feed7, conf: 101, ema_price: -42, ema_conf: 42, expo: -3, status: PriceStatus::Trading, num_publishers: 123212u32, max_num_publishers: 321232u32, attestation_time: (0xdeadbeeffadedeedu64) as i64, publish_time: 0xdadebeefi64, prev_publish_time: 0xdeadbabei64, prev_price: 0xdeadfacebeefi64, prev_conf: 0xbadbadbeefu64, // I could do this all day -SD last_attested_publish_time: (0xdeadbeeffadedeafu64) as i64, } } #[test] fn test_attestation_serde() -> Result<(), ErrBox> { let product_id_bytes = [21u8; 32]; let price_id_bytes = [222u8; 32]; let attestation: PriceAttestation = mock_attestation(Some(product_id_bytes), Some(price_id_bytes)); println!("Hex product_id: {:02X?}", &product_id_bytes); println!("Hex price_id: {:02X?}", &price_id_bytes); println!("Regular: {:#?}", &attestation); println!("Hex: {:#02X?}", &attestation); let bytes = attestation.serialize(); println!("Hex Bytes: {bytes:02X?}"); assert_eq!( PriceAttestation::deserialize(bytes.as_slice())?, attestation ); Ok(()) } #[test] fn test_attestation_serde_wrong_size() -> Result<(), ErrBox> { assert!(PriceAttestation::deserialize(&[][..]).is_err()); assert!(PriceAttestation::deserialize(vec![0u8; 1].as_slice()).is_err()); Ok(()) } #[test] fn test_batch_serde() -> Result<(), ErrBox> { let attestations: Vec<_> = (1..=3) .map(|i| { mock_attestation( Some([(i % 256) as u8; 32]), Some([(255 - (i % 256)) as u8; 32]), ) }) .collect(); let batch_attestation = BatchPriceAttestation { price_attestations: attestations, }; println!("Batch hex struct: {batch_attestation:#02X?}"); let serialized = batch_attestation.serialize()?; println!("Batch hex Bytes: {serialized:02X?}"); let deserialized: BatchPriceAttestation = BatchPriceAttestation::deserialize(serialized.as_slice())?; assert_eq!(batch_attestation, deserialized); Ok(()) } #[test] fn test_batch_serde_wrong_size() -> Result<(), ErrBox> { assert!(BatchPriceAttestation::deserialize(&[][..]).is_err()); assert!(BatchPriceAttestation::deserialize(vec![0u8; 1].as_slice()).is_err()); let attestations: Vec<_> = (0..20) .map(|i| mock_attestation(Some([(i % 256) as u8; 32]), None)) .collect(); let batch_attestation = BatchPriceAttestation { price_attestations: attestations, }; let serialized = batch_attestation.serialize()?; // Missing last byte in last attestation must be an error let len = serialized.len(); assert!(BatchPriceAttestation::deserialize(&serialized.as_slice()[..len - 1]).is_err()); Ok(()) } }