p2w attester contract use p2w-sdk (#68)
* Make solana pyth2wormhole contract to use the sdk
This commit is contained in:
parent
643ab162d1
commit
f72caf0b56
|
@ -44,24 +44,24 @@ ENV EMITTER_ADDRESS="11111111111111111111111111111115"
|
|||
ENV BRIDGE_ADDRESS="Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
|
||||
|
||||
# Build Wormhole Solana programs
|
||||
RUN --mount=type=cache,target=bridge/target \
|
||||
--mount=type=cache,target=modules/token_bridge/target \
|
||||
--mount=type=cache,target=modules/nft_bridge/target \
|
||||
--mount=type=cache,target=pyth2wormhole/target \
|
||||
--mount=type=cache,target=migration/target \
|
||||
cargo build-bpf --manifest-path "bridge/program/Cargo.toml" -- --locked && \
|
||||
cargo build-bpf --manifest-path "bridge/cpi_poster/Cargo.toml" -- --locked && \
|
||||
cargo build-bpf --manifest-path "modules/token_bridge/program/Cargo.toml" -- --locked && \
|
||||
cargo build-bpf --manifest-path "pyth2wormhole/program/Cargo.toml" -- --locked && \
|
||||
cargo build-bpf --manifest-path "modules/nft_bridge/program/Cargo.toml" -- --locked && \
|
||||
cargo build-bpf --manifest-path "migration/Cargo.toml" -- --locked && \
|
||||
cp bridge/target/deploy/bridge.so /opt/solana/deps/bridge.so && \
|
||||
cp bridge/target/deploy/cpi_poster.so /opt/solana/deps/cpi_poster.so && \
|
||||
cp migration/target/deploy/wormhole_migration.so /opt/solana/deps/wormhole_migration.so && \
|
||||
cp modules/token_bridge/target/deploy/token_bridge.so /opt/solana/deps/token_bridge.so && \
|
||||
cp modules/nft_bridge/target/deploy/nft_bridge.so /opt/solana/deps/nft_bridge.so && \
|
||||
cp modules/token_bridge/token-metadata/spl_token_metadata.so /opt/solana/deps/spl_token_metadata.so && \
|
||||
cp pyth2wormhole/target/deploy/pyth2wormhole.so /opt/solana/deps/pyth2wormhole.so
|
||||
RUN --mount=type=cache,target=solana/bridge/target \
|
||||
--mount=type=cache,target=solana/modules/token_bridge/target \
|
||||
--mount=type=cache,target=solana/modules/nft_bridge/target \
|
||||
--mount=type=cache,target=solana/pyth2wormhole/target \
|
||||
--mount=type=cache,target=solana/migration/target \
|
||||
cargo build-bpf --manifest-path "solana/bridge/program/Cargo.toml" -- --locked && \
|
||||
cargo build-bpf --manifest-path "solana/bridge/cpi_poster/Cargo.toml" -- --locked && \
|
||||
cargo build-bpf --manifest-path "solana/modules/token_bridge/program/Cargo.toml" -- --locked && \
|
||||
cargo build-bpf --manifest-path "solana/pyth2wormhole/program/Cargo.toml" -- --locked && \
|
||||
cargo build-bpf --manifest-path "solana/modules/nft_bridge/program/Cargo.toml" -- --locked && \
|
||||
cargo build-bpf --manifest-path "solana/migration/Cargo.toml" -- --locked && \
|
||||
cp solana/bridge/target/deploy/bridge.so /opt/solana/deps/bridge.so && \
|
||||
cp solana/bridge/target/deploy/cpi_poster.so /opt/solana/deps/cpi_poster.so && \
|
||||
cp solana/migration/target/deploy/wormhole_migration.so /opt/solana/deps/wormhole_migration.so && \
|
||||
cp solana/modules/token_bridge/target/deploy/token_bridge.so /opt/solana/deps/token_bridge.so && \
|
||||
cp solana/modules/nft_bridge/target/deploy/nft_bridge.so /opt/solana/deps/nft_bridge.so && \
|
||||
cp solana/modules/token_bridge/token-metadata/spl_token_metadata.so /opt/solana/deps/spl_token_metadata.so && \
|
||||
cp solana/pyth2wormhole/target/deploy/pyth2wormhole.so /opt/solana/deps/pyth2wormhole.so
|
||||
|
||||
# Build the Pyth Solana program
|
||||
WORKDIR $PYTH_DIR/pyth-client/program
|
4
Tiltfile
4
Tiltfile
|
@ -207,8 +207,8 @@ docker_build(
|
|||
|
||||
docker_build(
|
||||
ref = "solana-contract",
|
||||
context = "solana",
|
||||
dockerfile = "solana/Dockerfile",
|
||||
context = ".",
|
||||
dockerfile = "Dockerfile.solana",
|
||||
)
|
||||
|
||||
# solana local devnet
|
||||
|
|
|
@ -392,19 +392,6 @@ dependencies = [
|
|||
"syn 1.0.73",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"terminal_size",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.0"
|
||||
|
@ -603,7 +590,7 @@ version = "0.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61579ada4ec0c6031cfac3f86fdba0d195a7ebeb5e36693bd53cb5999a25beeb"
|
||||
dependencies = [
|
||||
"console 0.15.0",
|
||||
"console",
|
||||
"lazy_static",
|
||||
"tempfile",
|
||||
"zeroize",
|
||||
|
@ -1223,7 +1210,7 @@ version = "0.16.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b"
|
||||
dependencies = [
|
||||
"console 0.14.1",
|
||||
"console",
|
||||
"lazy_static",
|
||||
"number_prefix",
|
||||
"regex",
|
||||
|
@ -1607,6 +1594,16 @@ dependencies = [
|
|||
"syn 1.0.73",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p2w-sdk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"pyth-client 0.5.0",
|
||||
"serde",
|
||||
"solana-program",
|
||||
"solitaire",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.1"
|
||||
|
@ -1759,12 +1756,29 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44de48029c54ec1ca570786b5baeb906b0fc2409c8e0145585e287ee7a526c72"
|
||||
|
||||
[[package]]
|
||||
name = "pyth-client"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f779e98b8c8016d0c1409247a204bd4fcdea8b67ceeef545f04e324d66c49e52"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"borsh-derive",
|
||||
"bytemuck",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"solana-program",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyth2wormhole"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"pyth-client",
|
||||
"p2w-sdk",
|
||||
"pyth-client 0.2.2",
|
||||
"rocksalt",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
|
@ -1783,6 +1797,7 @@ dependencies = [
|
|||
"clap 3.0.0-beta.2",
|
||||
"env_logger 0.8.4",
|
||||
"log",
|
||||
"p2w-sdk",
|
||||
"pyth2wormhole",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
|
@ -2136,9 +2151,9 @@ checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.133"
|
||||
version = "1.0.136"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a"
|
||||
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
@ -2154,9 +2169,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.133"
|
||||
version = "1.0.136"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537"
|
||||
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.27",
|
||||
"quote 1.0.9",
|
||||
|
@ -2650,7 +2665,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "a03587d5bf5f7bc9302385f9ada8412662cdb93b5e3d40fee2a02553a932277c"
|
||||
dependencies = [
|
||||
"base32",
|
||||
"console 0.15.0",
|
||||
"console",
|
||||
"dialoguer",
|
||||
"hidapi",
|
||||
"log",
|
||||
|
|
|
@ -15,6 +15,7 @@ env_logger = "0.8.4"
|
|||
log = "0.4.14"
|
||||
wormhole-bridge-solana = {path = "../../bridge/program"}
|
||||
pyth2wormhole = {path = "../program"}
|
||||
p2w-sdk = { path = "../../../third_party/pyth/p2w-sdk/rust" }
|
||||
serde = "1"
|
||||
serde_yaml = "0.8"
|
||||
shellexpand = "2.1.0"
|
||||
|
|
|
@ -71,18 +71,16 @@ use bridge::{
|
|||
};
|
||||
|
||||
use pyth2wormhole::{
|
||||
attest::{
|
||||
P2WEmitter,
|
||||
P2W_MAX_BATCH_SIZE,
|
||||
},
|
||||
attest::P2W_MAX_BATCH_SIZE,
|
||||
config::P2WConfigAccount,
|
||||
initialize::InitializeAccounts,
|
||||
set_config::SetConfigAccounts,
|
||||
types::PriceAttestation,
|
||||
AttestData,
|
||||
Pyth2WormholeConfig,
|
||||
};
|
||||
|
||||
use p2w_sdk::P2WEmitter;
|
||||
|
||||
use crate::attestation_cfg::AttestationConfig;
|
||||
|
||||
pub type ErrBox = Box<dyn std::error::Error>;
|
||||
|
|
|
@ -22,6 +22,7 @@ rocksalt = { path = "../../solitaire/rocksalt" }
|
|||
solana-program = "=1.9.4"
|
||||
borsh = "=0.9.1"
|
||||
pyth-client = "0.2.2"
|
||||
p2w-sdk = { path = "../../../third_party/pyth/p2w-sdk/rust" }
|
||||
serde = { version = "1", optional = true}
|
||||
serde_derive = { version = "1", optional = true}
|
||||
serde_json = { version = "1", optional = true}
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
use crate::{
|
||||
config::P2WConfigAccount,
|
||||
types::{
|
||||
batch_serialize,
|
||||
PriceAttestation,
|
||||
},
|
||||
};
|
||||
use crate::config::P2WConfigAccount;
|
||||
use borsh::{
|
||||
BorshDeserialize,
|
||||
BorshSerialize,
|
||||
|
@ -24,6 +18,12 @@ use solana_program::{
|
|||
rent::Rent,
|
||||
};
|
||||
|
||||
use p2w_sdk::{
|
||||
BatchPriceAttestation,
|
||||
PriceAttestation,
|
||||
P2WEmitter,
|
||||
};
|
||||
|
||||
use bridge::{
|
||||
accounts::BridgeData,
|
||||
types::ConsistencyLevel,
|
||||
|
@ -50,8 +50,6 @@ use solitaire::{
|
|||
ToInstruction,
|
||||
};
|
||||
|
||||
pub type P2WEmitter<'b> = Derive<Info<'b>, "p2w-emitter">;
|
||||
|
||||
/// Important: must be manually maintained until native Solitaire
|
||||
/// variable len vector support.
|
||||
///
|
||||
|
@ -209,7 +207,11 @@ pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> So
|
|||
price.key.clone(),
|
||||
accs.clock.unix_timestamp,
|
||||
&*price.try_borrow_data()?,
|
||||
)?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
trace!(e.to_string());
|
||||
ProgramError::InvalidAccountData
|
||||
})?;
|
||||
|
||||
// The following check is crucial against poorly ordered
|
||||
// account inputs, e.g. [Some(prod1), Some(price1),
|
||||
|
@ -230,6 +232,10 @@ pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> So
|
|||
attestations.push(attestation);
|
||||
}
|
||||
|
||||
let batch_attestation = BatchPriceAttestation {
|
||||
price_attestations: attestations,
|
||||
};
|
||||
|
||||
trace!("Attestations successfully created");
|
||||
|
||||
let bridge_config = BridgeData::try_from_slice(&accs.wh_bridge.try_borrow_mut_data()?)?.config;
|
||||
|
@ -247,7 +253,7 @@ pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> So
|
|||
bridge::instruction::Instruction::PostMessage,
|
||||
PostMessageData {
|
||||
nonce: 0, // Superseded by the sequence number
|
||||
payload: batch_serialize(attestations.as_slice().iter()).map_err(|e| {
|
||||
payload: batch_attestation.serialize().map_err(|e| {
|
||||
trace!(e.to_string());
|
||||
ProgramError::InvalidAccountData
|
||||
})?,
|
||||
|
|
|
@ -3,7 +3,6 @@ pub mod attest;
|
|||
pub mod config;
|
||||
pub mod initialize;
|
||||
pub mod set_config;
|
||||
pub mod types;
|
||||
|
||||
use solitaire::solitaire;
|
||||
|
||||
|
|
|
@ -1,653 +0,0 @@
|
|||
//! 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 mod pyth_extensions;
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
convert::{
|
||||
TryFrom,
|
||||
TryInto,
|
||||
},
|
||||
io::Read,
|
||||
iter::Iterator,
|
||||
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,
|
||||
ErrBox,
|
||||
Result as SoliResult,
|
||||
SolitaireError,
|
||||
};
|
||||
|
||||
use self::pyth_extensions::{
|
||||
P2WCorpAction,
|
||||
P2WEma,
|
||||
P2WPriceStatus,
|
||||
P2WPriceType,
|
||||
};
|
||||
|
||||
/// 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 = 2;
|
||||
|
||||
pub const PUBKEY_LEN: usize = 32;
|
||||
|
||||
/// Decides the format of following bytes
|
||||
#[repr(u8)]
|
||||
pub enum PayloadId {
|
||||
PriceAttestation = 1, // Not in use, currently batch attestations imply PriceAttestation messages inside
|
||||
PriceBatchAttestation,
|
||||
}
|
||||
|
||||
// On-chain data types
|
||||
|
||||
/// 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.
|
||||
#[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,
|
||||
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,
|
||||
}
|
||||
|
||||
/// Turn a bunch of attestations into a combined payload.
|
||||
///
|
||||
/// Batches assume constant-size attestations within a single batch.
|
||||
pub fn batch_serialize(
|
||||
attestations: impl Iterator<Item = impl Borrow<PriceAttestation>>,
|
||||
) -> Result<Vec<u8>, ErrBox> {
|
||||
// magic
|
||||
let mut buf = P2W_MAGIC.to_vec();
|
||||
|
||||
// version
|
||||
buf.extend_from_slice(&P2W_FORMAT_VERSION.to_be_bytes()[..]);
|
||||
|
||||
// payload_id
|
||||
buf.push(PayloadId::PriceBatchAttestation as u8);
|
||||
|
||||
let collected: Vec<_> = attestations.collect();
|
||||
|
||||
// n_attestations
|
||||
buf.extend_from_slice(&(collected.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(collected.len());
|
||||
for (idx, a) in collected.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)
|
||||
}
|
||||
|
||||
/// Undo `batch_serialize`
|
||||
pub fn batch_deserialize(mut bytes: impl Read) -> Result<Vec<PriceAttestation>, 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 version_vec = vec![0u8; mem::size_of_val(&P2W_FORMAT_VERSION)];
|
||||
bytes.read_exact(version_vec.as_mut_slice())?;
|
||||
let 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::<PayloadId>()];
|
||||
bytes.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());
|
||||
}
|
||||
|
||||
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())?;
|
||||
|
||||
dbg!(&attestation_buf.len());
|
||||
|
||||
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(ret)
|
||||
}
|
||||
|
||||
impl PriceAttestation {
|
||||
pub fn from_pyth_price_bytes(
|
||||
price_id: Pubkey,
|
||||
timestamp: UnixTimestamp,
|
||||
value: &[u8],
|
||||
) -> Result<Self, SolitaireError> {
|
||||
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<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_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
|
||||
}
|
||||
pub fn deserialize(mut bytes: impl Read) -> Result<Self, ErrBox> {
|
||||
use P2WCorpAction::*;
|
||||
use P2WPriceStatus::*;
|
||||
use P2WPriceType::*;
|
||||
|
||||
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 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::<PayloadId>()];
|
||||
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::<P2WPriceType>()];
|
||||
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::<i64>()];
|
||||
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::<i32>()];
|
||||
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::<u64>()];
|
||||
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::<P2WPriceType>()];
|
||||
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::<P2WPriceType>()];
|
||||
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::<UnixTimestamp>()];
|
||||
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.
|
||||
fn parse_pyth_price(price_data: &[u8]) -> SoliResult<&Price> {
|
||||
if price_data.len() != mem::size_of::<Price>() {
|
||||
trace!(&format!(
|
||||
"parse_pyth_price: buffer length mismatch ({} expected, got {})",
|
||||
mem::size_of::<Price>(),
|
||||
price_data.len()
|
||||
));
|
||||
return Err(ProgramError::InvalidAccountData.into());
|
||||
}
|
||||
let price_account = pyth_client::cast::<Price>(price_data);
|
||||
|
||||
if price_account.atype != AccountType::Price as u32 {
|
||||
trace!(&format!(
|
||||
"parse_pyth_price: AccountType mismatch ({} expected, got {})",
|
||||
mem::size_of::<Price>(),
|
||||
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!(),
|
||||
],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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 = prod.unwrap_or([222u8; 32]);
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
#[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_parse_pyth_price() -> 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::<u8>() };
|
||||
|
||||
parse_pyth_price(bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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<_> = (0..65535)
|
||||
.map(|i| mock_attestation(Some([(i % 256) as u8; 32]), None))
|
||||
.collect();
|
||||
|
||||
let serialized = batch_serialize(attestations.iter())?;
|
||||
|
||||
let deserialized = batch_deserialize(serialized.as_slice())?;
|
||||
|
||||
assert_eq!(attestations, deserialized);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_batch_serde_wrong_size() -> Result<(), ErrBox> {
|
||||
assert!(batch_deserialize(&[][..]).is_err());
|
||||
assert!(batch_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 serialized = batch_serialize(attestations.iter())?;
|
||||
|
||||
// Missing last byte in last attestation must be an error
|
||||
let len = serialized.len();
|
||||
assert!(batch_deserialize(&serialized.as_slice()[..len - 1]).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
//! This module contains 1:1 (or close) copies of selected Pyth types
|
||||
//! with quick and dirty enhancements.
|
||||
|
||||
use std::{
|
||||
convert::TryInto,
|
||||
io::Read,
|
||||
mem,
|
||||
};
|
||||
|
||||
use pyth_client::{
|
||||
CorpAction,
|
||||
Ema,
|
||||
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,
|
||||
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)]
|
||||
#[cfg_attr(
|
||||
feature = "wasm",
|
||||
derive(serde_derive::Serialize, serde_derive::Deserialize)
|
||||
)]
|
||||
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)]
|
||||
#[cfg_attr(
|
||||
feature = "wasm",
|
||||
derive(serde_derive::Serialize, serde_derive::Deserialize)
|
||||
)]
|
||||
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)]
|
||||
#[cfg_attr(
|
||||
feature = "wasm",
|
||||
derive(serde_derive::Serialize, serde_derive::Deserialize)
|
||||
)]
|
||||
#[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::<P2WEma>();
|
||||
let upstream_size = mem::size_of::<Ema>();
|
||||
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<Ema> for &P2WEma {
|
||||
fn into(self) -> Ema {
|
||||
let our_size = mem::size_of::<P2WEma>();
|
||||
let upstream_size = mem::size_of::<Ema>();
|
||||
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<u8> {
|
||||
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
|
||||
}
|
||||
|
||||
pub fn deserialize(mut bytes: impl Read) -> Result<Self, ErrBox> {
|
||||
let mut val_vec = vec![0u8; mem::size_of::<i64>()];
|
||||
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::<i64>()];
|
||||
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::<i64>()];
|
||||
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 })
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ RUN apt-get install -y python3
|
|||
|
||||
ADD third_party/pyth/pyth_utils.py /usr/src/pyth/pyth_utils.py
|
||||
ADD third_party/pyth/p2w_autoattest.py /usr/src/pyth/p2w_autoattest.py
|
||||
ADD third_party/pyth/p2w-sdk/rust /usr/src/third_party/pyth/p2w-sdk/rust
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
--mount=type=cache,target=target \
|
||||
|
|
Loading…
Reference in New Issue