#[cfg(feature = "anchor")]
use anchor_lang::solana_program;
use anyhow::{bail, Context, Error as AnyError};
use borsh::{BorshDeserialize, BorshSerialize};
use rust_decimal::prelude::*;
use solana_program::account_info::AccountInfo;
use solana_program::hash::hash;
use solana_program::sysvar;
use solana_program::sysvar::clock::Clock;
use solana_program::sysvar::instructions::get_instruction_relative;
use crate::prelude::*;
use crate::Instructions;
const VERIFY_BUNDLE_IX_DISCRIMINATOR: [u8; 8] = [136, 34, 166, 38, 233, 13, 248, 165];
#[allow(unused)]
const SLOTS_PER_EPOCH: u64 = 432_000;
#[derive(Clone, BorshSerialize, BorshDeserialize)]
pub struct FeedInfo {
pub checksum: [u8; 32], pub value: i128, pub min_oracle_samples: u8, }
impl FeedInfo {
pub const PACKED_SIZE: usize = 49;
pub fn packed_size() -> usize {
Self::PACKED_SIZE
}
pub fn feed_id(&self) -> [u8; 32] {
self.checksum
}
pub fn from_packed(buffer: &[u8]) -> Result<Self, AnyError> {
if buffer.len() != Self::PACKED_SIZE {
bail!("Invalid FeedInfo packed size");
}
let checksum = buffer[0..32].try_into().unwrap();
let value = i128::from_le_bytes(buffer[32..48].try_into().unwrap());
let min_oracle_samples = buffer[48];
Ok(Self {
checksum,
value,
min_oracle_samples,
})
}
pub fn to_packed(&self) -> [u8; Self::PACKED_SIZE] {
let mut buffer = [0u8; Self::PACKED_SIZE];
buffer[0..32].copy_from_slice(&self.checksum);
buffer[32..48].copy_from_slice(&self.value.to_le_bytes());
buffer[48] = self.min_oracle_samples;
buffer
}
pub fn value(&self) -> Decimal {
Decimal::from_i128_with_scale(self.value, PRECISION)
}
#[inline]
fn generate_combined_checksum(feed_infos: &[FeedInfo], signed_slothash: &[u8; 32]) -> [u8; 32] {
let total_size = 40 + feed_infos.len() * FeedInfo::PACKED_SIZE;
let mut buffer = Vec::with_capacity(total_size);
buffer.extend_from_slice(signed_slothash);
buffer.extend_from_slice(&[0; 8]); for info in feed_infos {
buffer.extend_from_slice(&info.to_packed());
}
hash(&buffer).to_bytes()
}
}
#[derive(Clone, BorshSerialize, BorshDeserialize)]
pub struct VerifiedBundle {
pub slot_lower: u8, pub feed_infos: Vec<FeedInfo>, pub verified_slot: u64,
pub verified_slothash: [u8; 32],
pub verification_count: u8,
}
impl VerifiedBundle {
pub fn slots_stale(&self, clock: &Clock) -> u64 {
let current_slot = clock.slot;
current_slot.checked_sub(self.verified_slot).unwrap()
}
pub fn feed(&self, feed_id: &[u8; 32]) -> Result<FeedInfo, AnyError> {
let info = self
.feed_infos
.iter()
.find(|info| info.feed_id() == *feed_id)
.context("Switchboard On-Demand FeedNotFound")?;
Ok(info.clone())
}
pub fn load_from_instruction(ix_sysvar: &AccountInfo) -> Result<Self, AnyError> {
let mut offset = -1;
while let Ok(ix) = get_instruction_relative(offset, ix_sysvar) {
offset -= 1;
if ix.program_id != SWITCHBOARD_ON_DEMAND_PROGRAM_ID.to_bytes().into() {
continue;
}
if ix.data.len() < 8 {
bail!("Switchboard On-Demand InvalidDiscriminator");
}
let discriminator = &ix.data[0..8];
if discriminator != VERIFY_BUNDLE_IX_DISCRIMINATOR {
bail!("Switchboard On-Demand InvalidDiscriminator");
}
let bundle = VerifiedBundle::try_from_slice(&ix.data[8..])
.context("Failed to deserialize VerifiedBundle")?;
return Ok(bundle);
}
bail!("Switchboard On-Demand instruction not found")
}
pub fn verify<'info>(
&mut self,
queue: &AccountInfo<'info>,
slothash_sysvar: &AccountInfo,
ix_sysvar: &AccountInfo,
) -> Result<(), AnyError> {
if *queue.owner != SWITCHBOARD_ON_DEMAND_PROGRAM_ID.to_bytes().into() {
bail!("Invalid queue account owner");
}
if slothash_sysvar.key != &sysvar::slot_hashes::ID.to_bytes().into() {
bail!("Invalid slothash sysvar account owner");
}
if ix_sysvar.key != &sysvar::instructions::ID.to_bytes().into() {
bail!("Invalid instructions sysvar account owner");
}
let parsed_signatures = Instructions::load_secp_ix(ix_sysvar)?;
let computed_checksum =
FeedInfo::generate_combined_checksum(&self.feed_infos, &self.verified_slothash);
for info in &self.feed_infos {
if parsed_signatures.len() < (info.min_oracle_samples as usize) {
bail!("Switchboard On-Demand InsufficientSamples");
}
}
let queue_buf = queue.data.borrow();
if queue_buf[..8] != *QUEUE_ACCOUNT_DISCRIMINATOR {
bail!("Switchboard On-Demand InvalidQueueAccountData");
}
let queue: &QueueAccountData = bytemuck::try_from_bytes(&queue_buf[8..])
.map_err(|_| AnyError::msg("Invalid QueueAccountData"))?;
let mut keyset = queue.oracle_signing_keys[0..queue.oracle_keys_len as usize].to_vec();
let mut found_keys: Vec<_> = Vec::with_capacity(parsed_signatures.len());
for parsed_sig in parsed_signatures {
if parsed_sig.message != computed_checksum {
bail!("Switchboard On-Demand ChecksumMismatch");
}
let eth_address = parsed_sig.eth_address;
found_keys.push(eth_address);
}
keyset.sort_unstable();
found_keys.sort_unstable();
let mut i = 0;
let mut j = 0;
while i < found_keys.len() && j < keyset.len() {
if found_keys[i] == keyset[j] {
self.verification_count = self.verification_count.checked_add(1).unwrap();
i += 1;
j += 1;
} else {
j += 1;
}
}
if i < found_keys.len() {
bail!("Switchboard On-Demand NotAllSignaturesFound");
}
Ok(())
}
}