pyth-crosschain/third_party/pyth/p2w-sdk/rust/src/lib.rs

581 lines
20 KiB
Rust

//! 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 solitaire::{
Derive,
Info,
};
use {
serde::{
Deserialize,
Serialize,
Serializer,
},
std::{
borrow::Borrow,
convert::TryInto,
io::Read,
iter::Iterator,
mem,
},
};
#[cfg(feature = "wasm")]
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
pub mod wasm;
#[cfg(feature = "wasm")]
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use wasm_bindgen::prelude::*;
pub type ErrBox = Box<dyn std::error::Error>;
/// 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_VER_MAJOR: u16 = 3;
/// Starting with v3, format introduces a minor version to mark forward-compatible iterations
pub const P2W_FORMAT_VER_MINOR: u16 = 0;
/// Starting with v3, format introduces append-only
/// forward-compatibility to the header. This is the current number of
/// bytes after the hdr_size field.
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<Info<'b>, "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,
}
/// Helper allowing ToString implementers to be serialized as strings accordingly
pub fn use_to_string<T, S>(val: &T, s: S) -> Result<S::Ok, S::Error>
where
T: ToString,
S: Serializer,
{
s.serialize_str(&val.to_string())
}
pub fn pubkey_to_hex<S>(val: &Identifier, s: S) -> Result<S::Ok, S::Error>
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<PriceAttestation>,
}
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<Vec<u8>, 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);
// 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<Self, ErrBox> {
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 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 {}, expected {}",
major_version, 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 {}, expected {} or more",
minor_version, P2W_FORMAT_VER_MINOR
)
.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::<PayloadId>()];
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,
value: &[u8],
) -> Result<Self, ErrBox> {
let price = pyth_sdk_solana::state::load_price_account(value)?;
Ok(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,
})
}
/// Serialize this attestation according to the Pyth-over-wormhole serialization format
pub fn serialize(&self) -> Vec<u8> {
// 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,
} = 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.clone() 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()[..]);
buf
}
pub fn deserialize(mut bytes: impl Read) -> Result<Self, ErrBox> {
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::<i64>()];
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::<u64>()];
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::<i32>()];
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::<i64>()];
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::<u64>()];
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::<u32>()];
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::<u32>()];
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::<UnixTimestamp>()];
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::<UnixTimestamp>()];
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::<UnixTimestamp>()];
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::<i64>()];
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::<u64>()];
bytes.read_exact(prev_conf_vec.as_mut_slice())?;
let prev_conf = u64::from_be_bytes(prev_conf_vec.as_slice().try_into()?);
Ok(Self {
product_id,
price_id,
price,
conf,
expo,
ema_price,
ema_conf,
status: status.into(),
num_publishers,
max_num_publishers,
attestation_time,
publish_time,
prev_publish_time,
prev_price,
prev_conf,
})
}
}
/// 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.into(),
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
}
}
#[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: {:02X?}", bytes);
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..=10)
.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: {:#02X?}", batch_attestation);
let serialized = batch_attestation.serialize()?;
println!("Batch hex Bytes: {:02X?}", serialized);
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(())
}
}