use core::ptr::read_unaligned;
use anyhow::{bail, Error as AnyError};
#[cfg(not(feature = "pinocchio"))]
use solana_program::sysvar::instructions::get_instruction_relative;
use crate::prelude::*;
use crate::AccountInfo;
use crate::{
borrow_account_data, check_p64_eq, check_pubkey_eq, get_account_key, solana_program,
AsAccountInfo,
};
#[allow(unused_imports)]
use crate::{ON_DEMAND_DEVNET_PID, ON_DEMAND_MAINNET_PID};
const SYSVAR_SLOT_LEN: u64 = 512;
#[derive(Clone)]
#[cfg(feature = "pinocchio")]
pub struct QuoteVerifier<'a> {
queue: Option<&'a AccountInfo>,
slothash_sysvar: Option<&'a AccountInfo>,
ix_sysvar: Option<&'a AccountInfo>,
clock_slot: Option<u64>,
max_age: u64,
}
#[derive(Clone)]
#[cfg(not(feature = "pinocchio"))]
pub struct QuoteVerifier<'a> {
queue: Option<AccountInfo<'a>>,
slothash_sysvar: Option<AccountInfo<'a>>,
ix_sysvar: Option<AccountInfo<'a>>,
clock_slot: Option<u64>,
max_age: u64,
}
#[cfg(feature = "pinocchio")]
impl<'a> Default for QuoteVerifier<'a> {
fn default() -> Self {
Self::new()
}
}
#[cfg(not(feature = "pinocchio"))]
impl<'a> Default for QuoteVerifier<'a> {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "pinocchio")]
impl<'a> QuoteVerifier<'a> {
#[inline(always)]
pub fn new() -> Self {
Self {
queue: None,
slothash_sysvar: None,
ix_sysvar: None,
clock_slot: None,
max_age: 30,
}
}
#[inline(always)]
pub fn queue(&mut self, account: &'a AccountInfo) -> &mut Self {
self.queue = Some(account);
self
}
#[inline(always)]
pub fn slothash_sysvar(&mut self, sysvar: &'a AccountInfo) -> &mut Self {
self.slothash_sysvar = Some(sysvar);
self
}
#[inline(always)]
pub fn ix_sysvar(&mut self, sysvar: &'a AccountInfo) -> &mut Self {
self.ix_sysvar = Some(sysvar);
self
}
#[inline(always)]
pub fn clock_slot(&mut self, clock_slot: u64) -> &mut Self {
self.clock_slot = Some(clock_slot);
self
}
#[inline(always)]
pub fn max_age(&mut self, max_age: u64) -> &mut Self {
self.max_age = max_age;
self
}
#[cfg(feature = "pinocchio")]
#[inline(always)]
pub fn verify_account<'data, T>(
&self,
oracle_account: &'data T,
) -> Result<OracleQuote<'data>, AnyError>
where
T: AsAccountInfo<'data>,
{
let account_info = oracle_account.as_account_info();
let oracle_data = unsafe { account_info.borrow_data_unchecked() };
if oracle_data.len() < 40 {
bail!(
"Oracle account too small: {} bytes, expected at least 40",
oracle_data.len()
);
}
unsafe {
if read_unaligned(oracle_data.as_ptr() as *const u64) != QUOTE_DISCRIMINATOR_U64_LE {
bail!("Invalid oracle account discriminator");
}
}
self.verify_delimited(&oracle_data[40..])
}
#[cfg(not(feature = "pinocchio"))]
#[inline(always)]
pub fn verify_account<'data, T>(
&self,
oracle_account: &'data T,
) -> Result<OracleQuote<'data>, AnyError>
where
T: AsAccountInfo<'data>,
{
let account_info = oracle_account.as_account_info();
let oracle_data = unsafe { &*account_info.data.as_ptr() };
if oracle_data.len() < 40 {
bail!(
"Oracle account too small: {} bytes, expected at least 40",
oracle_data.len()
);
}
unsafe {
if read_unaligned(oracle_data.as_ptr() as *const u64) != QUOTE_DISCRIMINATOR_U64_LE {
bail!("Invalid oracle account discriminator");
}
}
self.verify_delimited(&oracle_data[40..])
}
#[inline(always)]
pub fn verify_delimited<'data>(
&self,
data: &'data [u8],
) -> Result<OracleQuote<'data>, AnyError> {
if data.len() < 2 {
bail!("Data too small for length prefix: {} bytes", data.len());
}
unsafe {
let len = read_unaligned(data.as_ptr() as *const u16) as usize;
if data.len() < len + 2 {
bail!(
"Data length mismatch: expected {}, got {}",
len + 2,
data.len()
);
}
self.verify(&data[2..len + 2])
}
}
#[inline(always)]
pub fn parse_unverified<'data>(
&self,
data: &'data [u8],
) -> Result<OracleQuote<'data>, AnyError> {
let (parsed_sigs, sig_count, oracle_idxs, recent_slot, version) =
Ed25519Sysvar::parse_instruction(data)?;
if sig_count == 0 {
bail!("No signatures provided");
}
let reference_sig = &parsed_sigs[0];
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,
data,
))
}
#[cfg(feature = "pinocchio")]
#[inline(always)]
pub fn parse_account_unverified<'data, T>(
&self,
oracle_account: &'data T,
) -> Result<OracleQuote<'data>, AnyError>
where
T: AsAccountInfo<'data>,
{
let account_info = oracle_account.as_account_info();
let oracle_data = unsafe { account_info.borrow_data_unchecked() };
if oracle_data.len() < 40 {
bail!(
"Oracle account too small: {} bytes, expected at least 40",
oracle_data.len()
);
}
self.parse_unverified_delimited(&oracle_data[40..])
}
#[cfg(not(feature = "pinocchio"))]
#[inline(always)]
pub fn parse_account_unverified<'data, T>(
&self,
oracle_account: &'data T,
) -> Result<OracleQuote<'data>, AnyError>
where
T: AsAccountInfo<'data>,
{
let account_info = oracle_account.as_account_info();
let oracle_data = unsafe { &*account_info.data.as_ptr() };
if oracle_data.len() < 40 {
bail!(
"Oracle account too small: {} bytes, expected at least 40",
oracle_data.len()
);
}
self.parse_unverified_delimited(&oracle_data[40..])
}
#[inline(always)]
pub fn parse_unverified_delimited<'data>(
&self,
data: &'data [u8],
) -> Result<OracleQuote<'data>, AnyError> {
if data.len() < 2 {
bail!("Data too small for length prefix: {} bytes", data.len());
}
unsafe {
let len = read_unaligned(data.as_ptr() as *const u16) as usize;
if data.len() < len + 2 {
bail!(
"Data length mismatch: expected {}, got {}",
len + 2,
data.len()
);
}
self.parse_unverified(&data[2..len + 2])
}
}
pub fn verify<'data>(&self, data: &'data [u8]) -> Result<OracleQuote<'data>, AnyError> {
let (parsed_sigs, sig_count, oracle_idxs, recent_slot, version) =
Ed25519Sysvar::parse_instruction(data)?;
let queue = self
.queue
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Queue account not set"))?;
let slothash_sysvar = self
.slothash_sysvar
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Slothash sysvar not set"))?;
let clock_slot = self
.clock_slot
.ok_or_else(|| anyhow::anyhow!("Clock slot not set"))?;
if clock_slot < recent_slot || clock_slot - 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 = unsafe { borrow_account_data!(queue) };
if queue_buf.len() != 6280 {
bail!("Queue account too small: {} bytes", queue_buf.len());
}
let queue_data: &QueueAccountData =
unsafe { &*(queue_buf.as_ptr().add(8) as *const QueueAccountData) };
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;
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,
data,
))
}
#[inline(always)]
pub fn verify_instruction_at(&self, instruction_idx: i64) -> Result<OracleQuote<'_>, AnyError> {
use crate::Instructions;
let ix_sysvar = self
.ix_sysvar
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Instructions sysvar not set"))?;
let data = {
#[cfg(feature = "pinocchio")]
{
Instructions::extract_ix_data(*ix_sysvar, instruction_idx as usize)
}
#[cfg(not(feature = "pinocchio"))]
{
Instructions::extract_ix_data(ix_sysvar, instruction_idx as usize)
}
};
self.verify(data)
}
fn find_slothash_in_sysvar(
target_slot: u64,
slothash_sysvar: &AccountInfo,
) -> Result<[u8; 32], AnyError> {
assert!(check_pubkey_eq(
*get_account_key!(slothash_sysvar),
solana_program::sysvar::slot_hashes::ID
));
let slothash_data = unsafe { borrow_account_data!(slothash_sysvar) };
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");
}
}
#[cfg(not(feature = "pinocchio"))]
impl<'a> QuoteVerifier<'a> {
#[inline(always)]
pub fn new() -> Self {
Self {
queue: None,
slothash_sysvar: None,
ix_sysvar: None,
clock_slot: None,
max_age: 30,
}
}
#[inline(always)]
pub fn queue<T>(&mut self, account: T) -> &mut Self
where
T: AsAccountInfo<'a>,
{
self.queue = Some(account.as_account_info().clone());
self
}
#[inline(always)]
pub fn slothash_sysvar<T>(&mut self, sysvar: T) -> &mut Self
where
T: AsAccountInfo<'a>,
{
self.slothash_sysvar = Some(sysvar.as_account_info().clone());
self
}
#[inline(always)]
pub fn ix_sysvar<T>(&mut self, sysvar: T) -> &mut Self
where
T: AsAccountInfo<'a>,
{
self.ix_sysvar = Some(sysvar.as_account_info().clone());
self
}
#[inline(always)]
pub fn clock_slot(&mut self, clock_slot: u64) -> &mut Self {
self.clock_slot = Some(clock_slot);
self
}
#[inline(always)]
pub fn max_age(&mut self, max_age: u64) -> &mut Self {
self.max_age = max_age;
self
}
#[cfg(feature = "pinocchio")]
#[inline(always)]
pub fn verify_account<'data, T>(
&self,
queue: &[u8; 32],
oracle_account: &'data T,
) -> Result<OracleQuote<'data>, AnyError>
where
T: AsAccountInfo<'data>,
{
let account_info = oracle_account.as_account_info();
let oracle_data: &'data [u8] = unsafe {
let temp_borrow = account_info.borrow_data_unchecked();
std::slice::from_raw_parts(temp_borrow.as_ptr(), temp_borrow.len())
};
if oracle_data.len() < 40 {
bail!(
"Oracle account too small: {} bytes, expected at least 40",
oracle_data.len()
);
}
unsafe {
if read_unaligned(oracle_data.as_ptr() as *const u64) != QUOTE_DISCRIMINATOR_U64_LE {
bail!("Invalid oracle account discriminator");
}
let data_ptr = oracle_data.as_ptr().add(8) as *const u64;
let queue_ptr = queue.as_ptr() as *const u64;
if !check_p64_eq(data_ptr, queue_ptr) {
bail!("Oracle account does not belong to the specified queue");
}
}
self.verify_delimited(&oracle_data[40..])
}
#[cfg(not(feature = "pinocchio"))]
#[inline(always)]
pub fn verify_account<'data, T>(
&self,
queue: &[u8; 32],
oracle_account: &'data T,
) -> Result<OracleQuote<'data>, AnyError>
where
T: AsAccountInfo<'data>,
{
let account_info = oracle_account.as_account_info();
let oracle_data = unsafe { &*account_info.data.as_ptr() };
if oracle_data.len() < 40 {
bail!(
"Oracle account too small: {} bytes, expected at least 40",
oracle_data.len()
);
}
unsafe {
if read_unaligned(oracle_data.as_ptr() as *const u64) != QUOTE_DISCRIMINATOR_U64_LE {
bail!("Invalid oracle account discriminator");
}
let data_ptr = oracle_data.as_ptr().add(8) as *const u64;
let queue_ptr = queue.as_ptr() as *const u64;
if !check_p64_eq(data_ptr, queue_ptr) {
bail!("Oracle account does not belong to the specified queue");
}
}
self.verify_delimited(&oracle_data[40..])
}
#[inline(always)]
pub fn verify_delimited<'data>(
&self,
data: &'data [u8],
) -> Result<OracleQuote<'data>, AnyError> {
if data.len() < 2 {
bail!("Data too small for length prefix: {} bytes", data.len());
}
unsafe {
let len = read_unaligned(data.as_ptr() as *const u16) as usize;
if data.len() < len + 2 {
bail!(
"Data length mismatch: expected {}, got {}",
len + 2,
data.len()
);
}
self.verify(&data[2..len + 2])
}
}
#[inline(always)]
pub fn parse_unverified<'data>(
&self,
data: &'data [u8],
) -> Result<OracleQuote<'data>, AnyError> {
let (parsed_sigs, sig_count, oracle_idxs, recent_slot, version) =
Ed25519Sysvar::parse_instruction(data)?;
if sig_count == 0 {
bail!("No signatures provided");
}
let reference_sig = &parsed_sigs[0];
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,
data,
))
}
#[cfg(feature = "pinocchio")]
#[inline(always)]
pub fn parse_account_unverified<'data, T>(
&self,
oracle_account: &'data T,
) -> Result<OracleQuote<'data>, AnyError>
where
T: AsAccountInfo<'data>,
{
let account_info = oracle_account.as_account_info();
let oracle_data = account_info.borrow_data_unchecked();
if oracle_data.len() < 40 {
bail!(
"Oracle account too small: {} bytes, expected at least 40",
oracle_data.len()
);
}
self.parse_unverified_delimited(&oracle_data[40..])
}
#[cfg(not(feature = "pinocchio"))]
#[inline(always)]
pub fn parse_account_unverified<'data, T>(
&self,
oracle_account: &'data T,
) -> Result<OracleQuote<'data>, AnyError>
where
T: AsAccountInfo<'data>,
{
let account_info = oracle_account.as_account_info();
let oracle_data = unsafe { &*account_info.data.as_ptr() };
if oracle_data.len() < 40 {
bail!(
"Oracle account too small: {} bytes, expected at least 40",
oracle_data.len()
);
}
self.parse_unverified_delimited(&oracle_data[40..])
}
#[inline(always)]
pub fn parse_unverified_delimited<'data>(
&self,
data: &'data [u8],
) -> Result<OracleQuote<'data>, AnyError> {
if data.len() < 2 {
bail!("Data too small for length prefix: {} bytes", data.len());
}
unsafe {
let len = read_unaligned(data.as_ptr() as *const u16) as usize;
if data.len() < len + 2 {
bail!(
"Data length mismatch: expected {}, got {}",
len + 2,
data.len()
);
}
self.parse_unverified(&data[2..len + 2])
}
}
pub fn verify<'data>(&self, data: &'data [u8]) -> Result<OracleQuote<'data>, AnyError> {
let (parsed_sigs, sig_count, oracle_idxs, recent_slot, version) =
Ed25519Sysvar::parse_instruction(data)?;
let queue = self
.queue
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Queue account not set"))?;
let slothash_sysvar = self
.slothash_sysvar
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Slothash sysvar not set"))?;
let clock_slot = self
.clock_slot
.ok_or_else(|| anyhow::anyhow!("Clock slot not set"))?;
if clock_slot < recent_slot || clock_slot - 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 = borrow_account_data!(queue);
if queue_buf.len() != 6280 {
bail!("Queue account too small: {} bytes", queue_buf.len());
}
let queue_data: &QueueAccountData =
unsafe { &*(queue_buf.as_ptr().add(8) as *const QueueAccountData) };
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;
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,
data,
))
}
#[inline(always)]
pub fn verify_instruction_at(&self, instruction_idx: i64) -> Result<OracleQuote<'a>, AnyError> {
use crate::Instructions;
let ix_sysvar = self
.ix_sysvar
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Instructions sysvar not set"))?;
let data = {
#[cfg(feature = "pinocchio")]
{
Instructions::extract_ix_data(*ix_sysvar, instruction_idx as usize)
}
#[cfg(not(feature = "pinocchio"))]
{
Instructions::extract_ix_data(ix_sysvar, instruction_idx as usize)
}
};
self.verify(data)
}
fn find_slothash_in_sysvar(
target_slot: u64,
slothash_sysvar: &AccountInfo,
) -> Result<[u8; 32], AnyError> {
assert!(check_pubkey_eq(
*get_account_key!(slothash_sysvar),
solana_program::sysvar::slot_hashes::ID
));
let slothash_data = borrow_account_data!(slothash_sysvar);
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: AsAccountInfo<'a>,
{
#[cfg(feature = "pinocchio")]
{
use core::ptr::read_unaligned;
use solana_program::ed25519_program::ID as ED25519_PROGRAM_ID;
use solana_program::sysvar::instructions::ID as INSTRUCTIONS_SYSVAR_ID;
use crate::{borrow_account_data, check_pubkey_eq, get_account_key, Instructions};
let ix_sysvar_account = ix_sysvar.as_account_info();
assert!(check_pubkey_eq(
*get_account_key!(ix_sysvar_account),
INSTRUCTIONS_SYSVAR_ID
));
let num_instructions = unsafe {
let data = borrow_account_data!(ix_sysvar_account);
read_unaligned(data.as_ptr() as *const u16) as usize
};
if num_instructions == 0 {
return Err(solana_program::program_error::ProgramError::InvalidInstructionData);
}
let prev_instruction_idx = num_instructions - 1;
let data = Instructions::extract_ix_data(ix_sysvar_account, prev_instruction_idx);
use solana_program::instruction::Instruction;
Ok(Instruction {
program_id: ED25519_PROGRAM_ID,
accounts: vec![], data: data.to_vec(),
})
}
#[cfg(not(feature = "pinocchio"))]
get_instruction_relative(-1, ix_sysvar.as_account_info())
}