use crate::prelude::*;
#[cfg(feature = "anchor")]
use anchor_lang::solana_program;
use anyhow::{bail, Error as AnyError};
use solana_program::account_info::AccountInfo;
use solana_program::sysvar::instructions::get_instruction_relative;
use crate::check_pubkey_eq;
use crate::check_p64_eq;
use core::ptr::read_unaligned;
#[allow(unused_imports)]
use crate::{ON_DEMAND_MAINNET_PID, ON_DEMAND_DEVNET_PID};
const SYSVAR_SLOT_LEN: u64 = 512;
#[derive(Clone)]
pub struct QuoteVerifier<'info, 'a> {
queue: Option<&'a AccountInfo<'info>>,
slothash_sysvar: Option<&'a AccountInfo<'info>>,
ix_sysvar: Option<&'a AccountInfo<'info>>,
clock: Option<&'a solana_program::sysvar::clock::Clock>,
max_age: u64,
_phantom: core::marker::PhantomData<(&'a (), &'info ())>,
}
impl<'info, 'a> Default for QuoteVerifier<'info, 'a> {
fn default() -> Self {
Self::new()
}
}
impl<'info, 'a> QuoteVerifier<'info, 'a> {
#[inline(always)]
pub fn new() -> Self {
Self {
queue: None,
slothash_sysvar: None,
ix_sysvar: None,
clock: None,
max_age: 0,
_phantom: core::marker::PhantomData,
}
}
#[inline(always)]
pub fn queue<T>(&mut self, account: &'a T) -> &mut Self
where
T: AsRef<AccountInfo<'info>>,
{
self.queue = Some(account.as_ref());
self
}
#[inline(always)]
pub fn slothash_sysvar<T>(&mut self, sysvar: &'a T) -> &mut Self
where
T: AsRef<AccountInfo<'info>>,
{
self.slothash_sysvar = Some(sysvar.as_ref());
self
}
#[inline(always)]
pub fn ix_sysvar<T>(&mut self, sysvar: &'a T) -> &mut Self
where
T: AsRef<AccountInfo<'info>>,
{
self.ix_sysvar = Some(sysvar.as_ref());
self
}
#[inline(always)]
pub fn clock<T>(&mut self, clock: &'a T) -> &mut Self
where
T: AsRef<solana_program::sysvar::clock::Clock>,
{
self.clock = Some(clock.as_ref());
self
}
#[inline(always)]
pub fn max_age(&mut self, max_age: u64) -> &mut Self {
self.max_age = max_age;
self
}
#[inline(always)]
pub fn verify_account(&self, oracle_account: &AccountInfo<'a>) -> Result<OracleQuote<'a>, AnyError> {
unsafe {
let data = &(*oracle_account.data.as_ptr());
if data.len() < 8 {
bail!("Oracle account too small: {} bytes, expected at least 8", data.len());
}
if read_unaligned(data.as_ptr() as *const u64) != QUOTE_DISCRIMINATOR_U64_LE {
bail!("Invalid oracle account discriminator");
}
self.verify_delimited(&data[8..])
}
}
#[inline(always)]
pub fn verify_delimited<'data>(
&self,
data: &'data [u8],
) -> Result<OracleQuote<'data>, AnyError>
where
'data: 'a,
{
unsafe {
if data.len() < 2 {
bail!("Delimited data too small: {} bytes, expected at least 2", data.len());
}
let len = (data.as_ptr() as *const u16).read_unaligned() as usize;
if len + 2 > data.len() {
bail!("Invalid delimited length: {} exceeds buffer size {}", len + 2, data.len());
}
self.verify(&data[2..len + 2])
}
}
pub fn verify<'data>(
&self,
data: &'data [u8],
) -> Result<OracleQuote<'data>, AnyError>
where
'data: 'a,
{
let (parsed_sigs, sig_count, oracle_idxs, recent_slot, version) =
Ed25519Sysvar::parse_instruction(data)?;
let queue = self.queue.ok_or_else(|| anyhow::anyhow!("Queue account not set"))?;
let slothash_sysvar = self.slothash_sysvar.ok_or_else(|| anyhow::anyhow!("Slothash sysvar not set"))?;
let clock = self.clock.ok_or_else(|| anyhow::anyhow!("Clock sysvar not set"))?;
if self.max_age > 0 && clock.slot.saturating_sub(recent_slot) > self.max_age {
bail!("Quote is too old: recent_slot={}, current_slot={}, max_age={}",
recent_slot, clock.slot, self.max_age);
}
if sig_count == 0 {
bail!("No signatures provided");
}
let queue_buf = queue.data.borrow();
if queue_buf.len() < 8 + std::mem::size_of::<QueueAccountData>() {
bail!("Queue account too small: {} bytes", queue_buf.len());
}
let queue_data: &QueueAccountData = bytemuck::try_from_bytes(&queue_buf[8..])
.map_err(|e| anyhow::anyhow!("Failed to deserialize queue data: {}", e))?;
let reference_sig = &parsed_sigs[0];
let header = unsafe { reference_sig.quote_header() };
let target_slothash = &header.signed_slothash as *const _ as *const u64;
let found_slothash = &Self::find_slothash_in_sysvar(recent_slot, slothash_sysvar)? as *const _ as *const u64;
if crate::utils::is_devnet() {
assert!(check_pubkey_eq(queue.owner, &ON_DEMAND_DEVNET_PID));
} else {
assert!(check_pubkey_eq(queue.owner, &ON_DEMAND_MAINNET_PID));
}
assert!(check_pubkey_eq(slothash_sysvar.key, &solana_program::sysvar::slot_hashes::ID));
assert!(unsafe { check_p64_eq(found_slothash, target_slothash) });
for i in 0..sig_count {
let oracle_idx = (oracle_idxs[i as usize] as usize) % 30;
let expected_oracle_key = queue_data.ed25519_oracle_signing_keys[oracle_idx];
let actual_oracle_key = unsafe { parsed_sigs[i as usize].pubkey() };
assert!(unsafe { check_p64_eq(actual_oracle_key as *const _ as *const u64, &expected_oracle_key as *const _ as *const u64) });
}
let reference_feed_infos = unsafe { reference_sig.feed_infos() };
let feed_count = reference_feed_infos.len();
Ok(OracleQuote::new(
unsafe { reference_sig.quote_header() },
sig_count,
reference_feed_infos,
feed_count as u8,
oracle_idxs,
recent_slot,
version,
Some(data),
))
}
#[inline(always)]
pub fn load_and_verify(&self, instruction_idx: i64) -> Result<OracleQuote<'a>, AnyError> {
use crate::Instructions;
use solana_program::ed25519_program::ID as ED25519_PROGRAM_ID;
use solana_program::sysvar::instructions::ID as INSTRUCTIONS_SYSVAR_ID;
let ix_sysvar = self.ix_sysvar.ok_or_else(|| anyhow::anyhow!("Instructions sysvar not set"))?;
let _clock = self.clock.ok_or_else(|| anyhow::anyhow!("Clock sysvar not set"))?;
let (program_id, data) = Instructions::extract_ix_data(ix_sysvar, instruction_idx as usize)
.map_err(|e| anyhow::anyhow!("Failed to extract instruction data: {:?}", e))?;
assert!(
check_pubkey_eq(ix_sysvar.key, &INSTRUCTIONS_SYSVAR_ID) &&
check_pubkey_eq(program_id, &ED25519_PROGRAM_ID)
);
self.verify(data)
}
fn find_slothash_in_sysvar(
target_slot: u64,
slothash_sysvar: &AccountInfo,
) -> Result<[u8; 32], AnyError> {
let slothash_data = slothash_sysvar.data.borrow();
if slothash_data.len() < 8 {
bail!("Slothash sysvar too small: {} bytes", slothash_data.len());
}
let slot_data: &[SlotHash] = unsafe { std::mem::transmute(&slothash_data[8..]) };
let mut estimated_idx = ((slot_data[0].slot - target_slot) % SYSVAR_SLOT_LEN) as usize;
loop {
let slot_entry = &slot_data[estimated_idx];
if slot_entry.slot == target_slot {
return Ok(slot_entry.hash);
}
if estimated_idx == 0 {
break;
}
estimated_idx -= 1;
}
bail!("Slot not found in slothash sysvar");
}
}
#[inline(always)]
pub fn get_ed25519_instruction<'a, T>(
ix_sysvar: &T,
) -> Result<solana_program::instruction::Instruction, solana_program::program_error::ProgramError>
where
T: AsRef<AccountInfo<'a>>,
{
get_instruction_relative(-1, ix_sysvar.as_ref())
}