diff --git a/lazer/contracts/solana/programs/pyth-lazer-solana-contract/Cargo.toml b/lazer/contracts/solana/programs/pyth-lazer-solana-contract/Cargo.toml index ec0c0cf893..b8172aa6e6 100644 --- a/lazer/contracts/solana/programs/pyth-lazer-solana-contract/Cargo.toml +++ b/lazer/contracts/solana/programs/pyth-lazer-solana-contract/Cargo.toml @@ -1,8 +1,10 @@ [package] name = "pyth-lazer-solana-contract" -version = "0.1.0" -description = "Created with Anchor" +version = "0.2.0" edition = "2021" +description = "Pyth Lazer Solana contract and SDK." +license = "Apache-2.0" +repository = "https://github.com/pyth-network/pyth-crosschain" [lib] crate-type = ["cdylib", "lib"] @@ -17,4 +19,16 @@ no-log-ix-name = [] idl-build = ["anchor-lang/idl-build"] [dependencies] +pyth-lazer-protocol = { version = "0.1.2", path = "../../../../sdk/rust/protocol" } + anchor-lang = "0.29.0" +bytemuck = "1.4.0" +byteorder = "1.4.3" +thiserror = "1.0" +solana-program = "1.16" + +[dev-dependencies] +hex = "0.4.3" +solana-program-test = "1.18.26" +solana-sdk = "1.18.26" +tokio = { version = "1.40.0", features = ["full"] } diff --git a/lazer/contracts/solana/programs/pyth-lazer-solana-contract/src/lib.rs b/lazer/contracts/solana/programs/pyth-lazer-solana-contract/src/lib.rs index 1a91c04aa0..225a8d4d6e 100644 --- a/lazer/contracts/solana/programs/pyth-lazer-solana-contract/src/lib.rs +++ b/lazer/contracts/solana/programs/pyth-lazer-solana-contract/src/lib.rs @@ -1,27 +1,36 @@ +mod signature; + use { - anchor_lang::{prelude::*, solana_program::pubkey::PUBKEY_BYTES}, - std::mem::size_of, + crate::signature::VerifiedMessage, + anchor_lang::{ + prelude::*, solana_program::pubkey::PUBKEY_BYTES, system_program, Discriminator, + }, + std::{io::Cursor, mem::size_of}, }; -declare_id!("pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt"); +pub use { + crate::signature::{ed25519_program_args, Ed25519SignatureOffsets}, + pyth_lazer_protocol as protocol, +}; -pub mod storage { - use anchor_lang::declare_id; +use solana_program::pubkey; - declare_id!("3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL"); +declare_id!("pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt"); - #[test] - fn test_storage_id() { - use {crate::STORAGE_SEED, anchor_lang::prelude::Pubkey}; +pub const STORAGE_ID: Pubkey = pubkey!("3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL"); - assert_eq!( - Pubkey::find_program_address(&[STORAGE_SEED], &super::ID).0, - ID - ); - } +#[test] +fn test_ids() { + assert_eq!( + Pubkey::find_program_address(&[STORAGE_SEED], &ID).0, + STORAGE_ID + ); } +pub const ANCHOR_DISCRIMINATOR_BYTES: usize = 8; pub const MAX_NUM_TRUSTED_SIGNERS: usize = 2; +pub const SPACE_FOR_TRUSTED_SIGNERS: usize = 5; +pub const EXTRA_SPACE: usize = 100; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, AnchorSerialize, AnchorDeserialize)] pub struct TrustedSignerInfo { @@ -33,17 +42,41 @@ impl TrustedSignerInfo { const SERIALIZED_LEN: usize = PUBKEY_BYTES + size_of::(); } +/// TODO: remove this legacy storage type +#[derive(AnchorDeserialize)] +pub struct StorageV010 { + pub top_authority: Pubkey, + pub num_trusted_signers: u8, + pub trusted_signers: [TrustedSignerInfo; MAX_NUM_TRUSTED_SIGNERS], +} + +impl StorageV010 { + pub const SERIALIZED_LEN: usize = PUBKEY_BYTES + + size_of::() + + TrustedSignerInfo::SERIALIZED_LEN * MAX_NUM_TRUSTED_SIGNERS; + + pub fn initialized_trusted_signers(&self) -> &[TrustedSignerInfo] { + &self.trusted_signers[0..usize::from(self.num_trusted_signers)] + } +} + #[account] pub struct Storage { pub top_authority: Pubkey, + pub treasury: Pubkey, + pub single_update_fee_in_lamports: u64, pub num_trusted_signers: u8, - pub trusted_signers: [TrustedSignerInfo; MAX_NUM_TRUSTED_SIGNERS], + pub trusted_signers: [TrustedSignerInfo; SPACE_FOR_TRUSTED_SIGNERS], + pub _extra_space: [u8; EXTRA_SPACE], } impl Storage { const SERIALIZED_LEN: usize = PUBKEY_BYTES + + PUBKEY_BYTES + + size_of::() + size_of::() - + TrustedSignerInfo::SERIALIZED_LEN * MAX_NUM_TRUSTED_SIGNERS; + + TrustedSignerInfo::SERIALIZED_LEN * SPACE_FOR_TRUSTED_SIGNERS + + EXTRA_SPACE; pub fn initialized_trusted_signers(&self) -> &[TrustedSignerInfo] { &self.trusted_signers[0..usize::from(self.num_trusted_signers)] @@ -56,8 +89,48 @@ pub const STORAGE_SEED: &[u8] = b"storage"; pub mod pyth_lazer_solana_contract { use super::*; - pub fn initialize(ctx: Context, top_authority: Pubkey) -> Result<()> { + pub fn initialize( + ctx: Context, + top_authority: Pubkey, + treasury: Pubkey, + ) -> Result<()> { ctx.accounts.storage.top_authority = top_authority; + ctx.accounts.storage.treasury = treasury; + ctx.accounts.storage.single_update_fee_in_lamports = 1; + Ok(()) + } + + pub fn migrate_from_0_1_0(ctx: Context, treasury: Pubkey) -> Result<()> { + let old_data = ctx.accounts.storage.data.borrow(); + if old_data[0..ANCHOR_DISCRIMINATOR_BYTES] != Storage::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData.into()); + } + let old_storage = StorageV010::deserialize(&mut &old_data[ANCHOR_DISCRIMINATOR_BYTES..])?; + if old_storage.top_authority != ctx.accounts.top_authority.key() { + return Err(ProgramError::MissingRequiredSignature.into()); + } + drop(old_data); + + let space = ANCHOR_DISCRIMINATOR_BYTES + Storage::SERIALIZED_LEN; + ctx.accounts.storage.realloc(space, false)?; + let min_lamports = Rent::get()?.minimum_balance(space); + if ctx.accounts.storage.lamports() < min_lamports { + return Err(ProgramError::AccountNotRentExempt.into()); + } + + let mut new_storage = Storage { + top_authority: old_storage.top_authority, + treasury, + single_update_fee_in_lamports: 1, + num_trusted_signers: old_storage.num_trusted_signers, + trusted_signers: Default::default(), + _extra_space: [0; EXTRA_SPACE], + }; + new_storage.trusted_signers[..old_storage.trusted_signers.len()] + .copy_from_slice(&old_storage.trusted_signers); + new_storage.try_serialize(&mut Cursor::new( + &mut **ctx.accounts.storage.data.borrow_mut(), + ))?; Ok(()) } @@ -66,6 +139,9 @@ pub mod pyth_lazer_solana_contract { if num_trusted_signers > ctx.accounts.storage.trusted_signers.len() { return Err(ProgramError::InvalidAccountData.into()); } + if num_trusted_signers > MAX_NUM_TRUSTED_SIGNERS { + return Err(ProgramError::InvalidAccountData.into()); + } let mut trusted_signers = ctx.accounts.storage.trusted_signers[..num_trusted_signers].to_vec(); if expires_at == 0 { @@ -92,6 +168,9 @@ pub mod pyth_lazer_solana_contract { if trusted_signers.len() > ctx.accounts.storage.trusted_signers.len() { return Err(ProgramError::AccountDataTooSmall.into()); } + if trusted_signers.len() > MAX_NUM_TRUSTED_SIGNERS { + return Err(ProgramError::InvalidInstructionData.into()); + } ctx.accounts.storage.trusted_signers = Default::default(); ctx.accounts.storage.trusted_signers[..trusted_signers.len()] @@ -102,6 +181,47 @@ pub mod pyth_lazer_solana_contract { .expect("num signers overflow"); Ok(()) } + + /// Verifies a ed25519 signature on Solana by checking that the transaction contains + /// a correct call to the built-in `ed25519_program`. + /// + /// - `message_data` is the signed message that is being verified. + /// - `ed25519_instruction_index` is the index of the `ed25519_program` instruction + /// within the transaction. This instruction must precede the current instruction. + /// - `signature_index` is the index of the signature within the inputs to the `ed25519_program`. + /// - `message_offset` is the offset of the signed message within the + /// input data for the current instruction. + pub fn verify_message( + ctx: Context, + message_data: Vec, + ed25519_instruction_index: u16, + signature_index: u8, + message_offset: u16, + ) -> Result { + system_program::transfer( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + system_program::Transfer { + from: ctx.accounts.payer.to_account_info(), + to: ctx.accounts.treasury.to_account_info(), + }, + ), + ctx.accounts.storage.single_update_fee_in_lamports, + )?; + + signature::verify_message( + &ctx.accounts.storage, + &ctx.accounts.instructions_sysvar, + &message_data, + ed25519_instruction_index, + signature_index, + message_offset, + ) + .map_err(|err| { + msg!("signature verification error: {:?}", err); + err.into() + }) + } } #[derive(Accounts)] @@ -111,7 +231,7 @@ pub struct Initialize<'info> { #[account( init, payer = payer, - space = 8 + Storage::SERIALIZED_LEN, + space = ANCHOR_DISCRIMINATOR_BYTES + Storage::SERIALIZED_LEN, seeds = [STORAGE_SEED], bump, )] @@ -119,6 +239,19 @@ pub struct Initialize<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct MigrateFrom010<'info> { + pub top_authority: Signer<'info>, + #[account( + mut, + seeds = [STORAGE_SEED], + bump, + )] + /// CHECK: top_authority in storage must match top_authority account. + pub storage: AccountInfo<'info>, + pub system_program: Program<'info, System>, +} + #[derive(Accounts)] pub struct Update<'info> { pub top_authority: Signer<'info>, @@ -130,3 +263,22 @@ pub struct Update<'info> { )] pub storage: Account<'info, Storage>, } + +#[derive(Accounts)] +pub struct VerifyMessage<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + seeds = [STORAGE_SEED], + bump, + has_one = treasury + )] + pub storage: Account<'info, Storage>, + /// CHECK: this account doesn't need additional constraints. + pub treasury: AccountInfo<'info>, + pub system_program: Program<'info, System>, + /// CHECK: account ID is checked in Solana SDK during calls + /// (e.g. in `sysvar::instructions::load_instruction_at_checked`). + /// This account is not usable with anchor's `Program` account type because it's not executable. + pub instructions_sysvar: AccountInfo<'info>, +} diff --git a/lazer/contracts/solana/programs/pyth-lazer-solana-contract/src/signature.rs b/lazer/contracts/solana/programs/pyth-lazer-solana-contract/src/signature.rs new file mode 100644 index 0000000000..62a71e8402 --- /dev/null +++ b/lazer/contracts/solana/programs/pyth-lazer-solana-contract/src/signature.rs @@ -0,0 +1,299 @@ +use { + crate::Storage, + anchor_lang::{ + prelude::{borsh, AccountInfo, Clock, ProgramError, Pubkey, SolanaSysvar}, + solana_program::{ed25519_program, pubkey::PUBKEY_BYTES, sysvar}, + AnchorDeserialize, AnchorSerialize, + }, + bytemuck::{cast_slice, checked::try_cast_slice, Pod, Zeroable}, + byteorder::{ByteOrder, LE}, + thiserror::Error, +}; + +const ED25519_PROGRAM_INPUT_HEADER_LEN: usize = 2; + +const SIGNATURE_LEN: u16 = 64; +const PUBKEY_LEN: u16 = 32; +const MAGIC_LEN: u16 = 4; +const MESSAGE_SIZE_LEN: u16 = 2; + +/// Part of the inputs to the built-in `ed25519_program` on Solana that represents a single +/// signature verification request. +/// +/// `ed25519_program` does not receive the signature data directly. Instead, it receives +/// these fields that indicate the location of the signature data within data of other +/// instructions within the same transaction. +#[derive(Debug, Clone, Copy, Zeroable, Pod)] +#[repr(C)] +pub struct Ed25519SignatureOffsets { + /// Offset to the ed25519 signature within the instruction data. + pub signature_offset: u16, + /// Index of the instruction that contains the signature. + pub signature_instruction_index: u16, + /// Offset to the public key within the instruction data. + pub public_key_offset: u16, + /// Index of the instruction that contains the public key. + pub public_key_instruction_index: u16, + /// Offset to the signed payload within the instruction data. + pub message_data_offset: u16, + // Size of the signed payload. + pub message_data_size: u16, + /// Index of the instruction that contains the signed payload. + pub message_instruction_index: u16, +} + +impl Ed25519SignatureOffsets { + /// Sets up `Ed25519SignatureOffsets` for verifying the Pyth Lazer message signature. + /// - `message` is the Pyth Lazer message being sent. + /// - `instruction_index` is the index of that instruction within the transaction. + /// - `starting_offset` is the offset of the Pyth Lazer message within the instruction data. + /// + /// Panics if `starting_offset` is invalid or the `instruction_data` is not long enough to + /// contain the message. + pub fn new(message: &[u8], instruction_index: u16, starting_offset: u16) -> Self { + let signature_offset = starting_offset + MAGIC_LEN; + let public_key_offset = signature_offset + SIGNATURE_LEN; + let message_data_size_offset = public_key_offset + PUBKEY_LEN; + let message_data_offset = message_data_size_offset + MESSAGE_SIZE_LEN; + let message_data_size = LE::read_u16( + &message[(message_data_size_offset - starting_offset).into() + ..(message_data_offset - starting_offset).into()], + ); + Ed25519SignatureOffsets { + signature_offset, + signature_instruction_index: instruction_index, + public_key_offset, + public_key_instruction_index: instruction_index, + message_data_offset, + message_data_size, + message_instruction_index: instruction_index, + } + } +} + +/// Creates inputs to the built-in `ed25519_program` on Solana that verifies signatures. +pub fn ed25519_program_args(signatures: &[Ed25519SignatureOffsets]) -> Vec { + let padding = 0u8; + let mut signature_args = vec![ + signatures.len().try_into().expect("too many signatures"), + padding, + ]; + signature_args.extend_from_slice(cast_slice(signatures)); + signature_args +} + +/// A message with a verified ed25519 signature. +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct VerifiedMessage { + /// Public key that signed the message. + pub public_key: Pubkey, + /// Signed message payload. + pub payload: Vec, +} + +#[derive(Debug, Error)] +pub enum SignatureVerificationError { + #[error("ed25519 instruction must precede current instruction")] + Ed25519InstructionMustPrecedeCurrentInstruction, + #[error("load instruction at failed")] + LoadInstructionAtFailed(#[source] ProgramError), + #[error("load current index failed")] + LoadCurrentIndexFailed(#[source] ProgramError), + #[error("load current index failed")] + ClockGetFailed(#[source] ProgramError), + #[error("invalid ed25519 instruction program")] + InvalidEd25519InstructionProgramId, + #[error("invalid ed25519 instruction data length")] + InvalidEd25519InstructionDataLength, + #[error("invalid signature index")] + InvalidSignatureIndex, + #[error("invalid signature offset")] + InvalidSignatureOffset, + #[error("invalid public key offset")] + InvalidPublicKeyOffset, + #[error("invalid message offset")] + InvalidMessageOffset, + #[error("invalid message data size")] + InvalidMessageDataSize, + #[error("invalid instruction index")] + InvalidInstructionIndex, + #[error("message offset overflow")] + MessageOffsetOverflow, + #[error("format magic mismatch")] + FormatMagicMismatch, + #[error("invalid storage account id")] + InvalidStorageAccountId, + #[error("invalid storage data")] + InvalidStorageData, + #[error("not a trusted signer")] + NotTrustedSigner, +} + +impl From for ProgramError { + fn from(value: SignatureVerificationError) -> Self { + match value { + SignatureVerificationError::LoadInstructionAtFailed(e) + | SignatureVerificationError::ClockGetFailed(e) + | SignatureVerificationError::LoadCurrentIndexFailed(e) => e, + SignatureVerificationError::InvalidStorageData => ProgramError::InvalidAccountData, + SignatureVerificationError::NotTrustedSigner => ProgramError::MissingRequiredSignature, + _ => ProgramError::InvalidInstructionData, + } + } +} + +impl From for anchor_lang::error::Error { + fn from(value: SignatureVerificationError) -> Self { + ProgramError::from(value).into() + } +} + +/// Verifies a ed25519 signature on Solana by checking that the transaction contains +/// a correct call to the built-in `ed25519_program`. +/// +/// - `message_data` is the signed message that is being verified. +/// - `ed25519_instruction_index` is the index of the `ed25519_program` instruction +/// within the transaction. This instruction must precede the current instruction. +/// - `signature_index` is the index of the signature within the inputs to the `ed25519_program`. +/// - `message_offset` is the offset of the signed message within the +/// input data for the current instruction. +pub fn verify_message( + storage: &Storage, + instructions_sysvar: &AccountInfo, + message_data: &[u8], + ed25519_instruction_index: u16, + signature_index: u8, + message_offset: u16, +) -> Result { + const SOLANA_FORMAT_MAGIC_LE: u32 = 2182742457; + + let self_instruction_index = + sysvar::instructions::load_current_index_checked(instructions_sysvar) + .map_err(SignatureVerificationError::LoadCurrentIndexFailed)?; + + if ed25519_instruction_index >= self_instruction_index { + return Err(SignatureVerificationError::Ed25519InstructionMustPrecedeCurrentInstruction); + } + + let instruction = sysvar::instructions::load_instruction_at_checked( + ed25519_instruction_index.into(), + instructions_sysvar, + ) + .map_err(SignatureVerificationError::LoadInstructionAtFailed)?; + + if instruction.program_id != ed25519_program::ID { + return Err(SignatureVerificationError::InvalidEd25519InstructionProgramId); + } + if instruction.data.len() < ED25519_PROGRAM_INPUT_HEADER_LEN { + return Err(SignatureVerificationError::InvalidEd25519InstructionDataLength); + } + + let num_signatures = instruction.data[0]; + if signature_index >= num_signatures { + return Err(SignatureVerificationError::InvalidSignatureIndex); + } + let args: &[Ed25519SignatureOffsets] = + try_cast_slice(&instruction.data[ED25519_PROGRAM_INPUT_HEADER_LEN..]) + .map_err(|_| SignatureVerificationError::InvalidEd25519InstructionDataLength)?; + + let args_len = args + .len() + .try_into() + .map_err(|_| SignatureVerificationError::InvalidEd25519InstructionDataLength)?; + if signature_index >= args_len { + return Err(SignatureVerificationError::InvalidSignatureIndex); + } + let offsets = &args[usize::from(signature_index)]; + + let expected_signature_offset = message_offset + .checked_add(MAGIC_LEN) + .ok_or(SignatureVerificationError::MessageOffsetOverflow)?; + if offsets.signature_offset != expected_signature_offset { + return Err(SignatureVerificationError::InvalidSignatureOffset); + } + + let magic = LE::read_u32(&message_data[..MAGIC_LEN.into()]); + if magic != SOLANA_FORMAT_MAGIC_LE { + return Err(SignatureVerificationError::FormatMagicMismatch); + } + + let expected_public_key_offset = expected_signature_offset + .checked_add(SIGNATURE_LEN) + .ok_or(SignatureVerificationError::MessageOffsetOverflow)?; + if offsets.public_key_offset != expected_public_key_offset { + return Err(SignatureVerificationError::InvalidPublicKeyOffset); + } + + let expected_message_size_offset = expected_public_key_offset + .checked_add(PUBKEY_LEN) + .ok_or(SignatureVerificationError::MessageOffsetOverflow)?; + + let expected_message_data_offset = expected_message_size_offset + .checked_add(MESSAGE_SIZE_LEN) + .ok_or(SignatureVerificationError::MessageOffsetOverflow)?; + if offsets.message_data_offset != expected_message_data_offset { + return Err(SignatureVerificationError::InvalidMessageOffset); + } + + let expected_message_size = { + let start = usize::from( + expected_message_size_offset + .checked_sub(message_offset) + .unwrap(), + ); + let end = usize::from( + expected_message_data_offset + .checked_sub(message_offset) + .unwrap(), + ); + LE::read_u16(&message_data[start..end]) + }; + if offsets.message_data_size != expected_message_size { + return Err(SignatureVerificationError::InvalidMessageDataSize); + } + if offsets.signature_instruction_index != self_instruction_index + || offsets.public_key_instruction_index != self_instruction_index + || offsets.message_instruction_index != self_instruction_index + { + return Err(SignatureVerificationError::InvalidInstructionIndex); + } + + let public_key = { + let start = usize::from( + expected_public_key_offset + .checked_sub(message_offset) + .unwrap(), + ); + let end = start + .checked_add(PUBKEY_BYTES) + .ok_or(SignatureVerificationError::MessageOffsetOverflow)?; + &message_data[start..end] + }; + let now = Clock::get() + .map_err(SignatureVerificationError::ClockGetFailed)? + .unix_timestamp; + if !storage + .initialized_trusted_signers() + .iter() + .any(|s| s.pubkey.as_ref() == public_key && s.expires_at > now) + { + return Err(SignatureVerificationError::NotTrustedSigner); + } + + let payload = { + let start = usize::from( + expected_message_data_offset + .checked_sub(message_offset) + .unwrap(), + ); + let end = start + .checked_add(expected_message_size.into()) + .ok_or(SignatureVerificationError::MessageOffsetOverflow)?; + &message_data[start..end] + }; + + Ok(VerifiedMessage { + public_key: Pubkey::new_from_array(public_key.try_into().unwrap()), + payload: payload.to_vec(), + }) +} diff --git a/lazer/sdk/rust/protocol/Cargo.toml b/lazer/sdk/rust/protocol/Cargo.toml index a616da7958..f6b2be24da 100644 --- a/lazer/sdk/rust/protocol/Cargo.toml +++ b/lazer/sdk/rust/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyth-lazer-protocol" -version = "0.1.0" +version = "0.1.2" edition = "2021" [dependencies] @@ -10,3 +10,6 @@ serde = { version = "1.0.209", features = ["derive"] } derive_more = { version = "0.99.18", features = ["from"] } itertools = "0.13.0" rust_decimal = "1.36.0" + +[dev-dependencies] +bincode = "1.3.3" diff --git a/lazer/sdk/rust/protocol/src/lib.rs b/lazer/sdk/rust/protocol/src/lib.rs index f81fca98a5..2f2f1e9f19 100644 --- a/lazer/sdk/rust/protocol/src/lib.rs +++ b/lazer/sdk/rust/protocol/src/lib.rs @@ -1,7 +1,9 @@ //! Protocol types. +pub mod message; pub mod payload; pub mod publisher; pub mod router; +mod serde_price_as_i64; mod serde_str; pub mod subscription; diff --git a/lazer/sdk/rust/protocol/src/message.rs b/lazer/sdk/rust/protocol/src/message.rs new file mode 100644 index 0000000000..c3413fe65b --- /dev/null +++ b/lazer/sdk/rust/protocol/src/message.rs @@ -0,0 +1,113 @@ +use { + crate::payload::{EVM_FORMAT_MAGIC, SOLANA_FORMAT_MAGIC_LE}, + anyhow::bail, + byteorder::{ReadBytesExt, WriteBytesExt, BE, LE}, + std::io::{Cursor, Read, Write}, +}; + +/// EVM signature enveope. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct EvmMessage { + pub payload: Vec, + pub signature: [u8; 64], + pub recovery_id: u8, +} + +impl EvmMessage { + pub fn serialize(&self, mut writer: impl Write) -> anyhow::Result<()> { + writer.write_u32::(EVM_FORMAT_MAGIC)?; + writer.write_all(&self.signature)?; + writer.write_u8(self.recovery_id)?; + writer.write_u16::(self.payload.len().try_into()?)?; + writer.write_all(&self.payload)?; + Ok(()) + } + + pub fn deserialize_slice(data: &[u8]) -> anyhow::Result { + Self::deserialize(Cursor::new(data)) + } + + pub fn deserialize(mut reader: impl Read) -> anyhow::Result { + let magic = reader.read_u32::()?; + if magic != EVM_FORMAT_MAGIC { + bail!("magic mismatch"); + } + let mut signature = [0u8; 64]; + reader.read_exact(&mut signature)?; + let recovery_id = reader.read_u8()?; + let payload_len: usize = reader.read_u16::()?.into(); + let mut payload = vec![0u8; payload_len]; + reader.read_exact(&mut payload)?; + Ok(Self { + payload, + signature, + recovery_id, + }) + } +} + +/// Solana signature envelope. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SolanaMessage { + pub payload: Vec, + pub signature: [u8; 64], + pub public_key: [u8; 32], +} + +impl SolanaMessage { + pub fn serialize(&self, mut writer: impl Write) -> anyhow::Result<()> { + writer.write_u32::(SOLANA_FORMAT_MAGIC_LE)?; + writer.write_all(&self.signature)?; + writer.write_all(&self.public_key)?; + writer.write_u16::(self.payload.len().try_into()?)?; + writer.write_all(&self.payload)?; + Ok(()) + } + + pub fn deserialize_slice(data: &[u8]) -> anyhow::Result { + Self::deserialize(Cursor::new(data)) + } + + pub fn deserialize(mut reader: impl Read) -> anyhow::Result { + let magic = reader.read_u32::()?; + if magic != SOLANA_FORMAT_MAGIC_LE { + bail!("magic mismatch"); + } + let mut signature = [0u8; 64]; + reader.read_exact(&mut signature)?; + let mut public_key = [0u8; 32]; + reader.read_exact(&mut public_key)?; + let payload_len: usize = reader.read_u16::()?.into(); + let mut payload = vec![0u8; payload_len]; + reader.read_exact(&mut payload)?; + Ok(Self { + payload, + signature, + public_key, + }) + } +} + +#[test] +fn test_evm_serde() { + let m1 = EvmMessage { + payload: vec![1, 2, 4, 3], + signature: [5; 64], + recovery_id: 1, + }; + let mut buf = Vec::new(); + m1.serialize(&mut buf).unwrap(); + assert_eq!(m1, EvmMessage::deserialize_slice(&buf).unwrap()); +} + +#[test] +fn test_solana_serde() { + let m1 = SolanaMessage { + payload: vec![1, 2, 4, 3], + signature: [5; 64], + public_key: [6; 32], + }; + let mut buf = Vec::new(); + m1.serialize(&mut buf).unwrap(); + assert_eq!(m1, SolanaMessage::deserialize_slice(&buf).unwrap()); +} diff --git a/lazer/sdk/rust/protocol/src/publisher.rs b/lazer/sdk/rust/protocol/src/publisher.rs index d475456986..b53c33d616 100644 --- a/lazer/sdk/rust/protocol/src/publisher.rs +++ b/lazer/sdk/rust/protocol/src/publisher.rs @@ -18,13 +18,64 @@ pub struct PriceFeedData { pub source_timestamp_us: TimestampUs, /// Timestamp of the last update provided by the publisher. pub publisher_timestamp_us: TimestampUs, - /// Last known value of the "main" (?) price of this price feed. + /// Last known value of the best executable price of this price feed. /// `None` if no value is currently available. + #[serde(with = "crate::serde_price_as_i64")] pub price: Option, /// Last known value of the best bid price of this price feed. /// `None` if no value is currently available. + #[serde(with = "crate::serde_price_as_i64")] pub best_bid_price: Option, /// Last known value of the best ask price of this price feed. /// `None` if no value is currently available. + #[serde(with = "crate::serde_price_as_i64")] pub best_ask_price: Option, } + +#[test] +fn price_feed_data_serde() { + let data = [ + 1, 0, 0, 0, // price_feed_id + 2, 0, 0, 0, 0, 0, 0, 0, // source_timestamp_us + 3, 0, 0, 0, 0, 0, 0, 0, // publisher_timestamp_us + 4, 0, 0, 0, 0, 0, 0, 0, // price + 5, 0, 0, 0, 0, 0, 0, 0, // best_bid_price + 6, 2, 0, 0, 0, 0, 0, 0, // best_ask_price + ]; + + let expected = PriceFeedData { + price_feed_id: PriceFeedId(1), + source_timestamp_us: TimestampUs(2), + publisher_timestamp_us: TimestampUs(3), + price: Some(Price(4.try_into().unwrap())), + best_bid_price: Some(Price(5.try_into().unwrap())), + best_ask_price: Some(Price((2 * 256 + 6).try_into().unwrap())), + }; + assert_eq!( + bincode::deserialize::(&data).unwrap(), + expected + ); + assert_eq!(bincode::serialize(&expected).unwrap(), data); + + let data2 = [ + 1, 0, 0, 0, // price_feed_id + 2, 0, 0, 0, 0, 0, 0, 0, // source_timestamp_us + 3, 0, 0, 0, 0, 0, 0, 0, // publisher_timestamp_us + 4, 0, 0, 0, 0, 0, 0, 0, // price + 0, 0, 0, 0, 0, 0, 0, 0, // best_bid_price + 0, 0, 0, 0, 0, 0, 0, 0, // best_ask_price + ]; + let expected2 = PriceFeedData { + price_feed_id: PriceFeedId(1), + source_timestamp_us: TimestampUs(2), + publisher_timestamp_us: TimestampUs(3), + price: Some(Price(4.try_into().unwrap())), + best_bid_price: None, + best_ask_price: None, + }; + assert_eq!( + bincode::deserialize::(&data2).unwrap(), + expected2 + ); + assert_eq!(bincode::serialize(&expected2).unwrap(), data2); +} diff --git a/lazer/sdk/rust/protocol/src/router.rs b/lazer/sdk/rust/protocol/src/router.rs index 947ee0ba4f..6ed1c83676 100644 --- a/lazer/sdk/rust/protocol/src/router.rs +++ b/lazer/sdk/rust/protocol/src/router.rs @@ -8,7 +8,7 @@ use { serde::{de::Error, Deserialize, Serialize}, std::{ num::NonZeroI64, - ops::{Add, Deref, DerefMut, Div, Sub}, + ops::{Add, Deref, DerefMut, Div, Mul, Sub}, time::{SystemTime, UNIX_EPOCH}, }, }; @@ -121,6 +121,21 @@ impl Div for Price { } } +impl Mul for Price { + type Output = Option; + fn mul(self, rhs: Price) -> Self::Output { + let left_value = i128::from(self.0.get()); + let right_value = i128::from(rhs.0.get()); + + let value = left_value * right_value / 10i128.pow(Price::TMP_EXPONENT); + let value = match value.try_into() { + Ok(value) => value, + Err(_) => return None, + }; + NonZeroI64::new(value).map(Self) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum PriceFeedProperty { diff --git a/lazer/sdk/rust/protocol/src/serde_price_as_i64.rs b/lazer/sdk/rust/protocol/src/serde_price_as_i64.rs new file mode 100644 index 0000000000..b2830c6d9f --- /dev/null +++ b/lazer/sdk/rust/protocol/src/serde_price_as_i64.rs @@ -0,0 +1,22 @@ +use { + crate::router::Price, + serde::{Deserialize, Deserializer, Serialize, Serializer}, + std::num::NonZeroI64, +}; + +pub fn serialize(value: &Option, serializer: S) -> Result +where + S: Serializer, +{ + value + .map_or(0i64, |price| price.0.get()) + .serialize(serializer) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = i64::deserialize(deserializer)?; + Ok(NonZeroI64::new(value).map(Price)) +}