switchboard-on-demand 0.6.0

A Rust library to interact with the Switchboard Solana program.
Documentation
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;
#[allow(unused_imports)]
use crate::{ON_DEMAND_MAINNET_PID, ON_DEMAND_DEVNET_PID};

const SYSVAR_SLOT_LEN: u64 = 512;
// const SBOD_DISCRIMINATOR: u32 = u32::from_le_bytes(*b"SBOD");

// Pre-computed expected data prefix (64 bytes: program ID + sysvar ID)
#[cfg(not(feature = "devnet"))]
const EXPECTED_PREFIX: [u8; 64] = {
    let mut prefix = [0u8; 64];
    let pid_bytes = ON_DEMAND_MAINNET_PID.to_bytes();
    let sysvar_bytes = solana_program::sysvar::slot_hashes::ID.to_bytes();

    // Copy program ID
    let mut i = 0;
    while i < 32 {
        prefix[i] = pid_bytes[i];
        i += 1;
    }

    // Copy sysvar ID
    i = 0;
    while i < 32 {
        prefix[32 + i] = sysvar_bytes[i];
        i += 1;
    }

    prefix
};

#[cfg(feature = "devnet")]
const EXPECTED_PREFIX: [u8; 64] = {
    let mut prefix = [0u8; 64];
    let pid_bytes = ON_DEMAND_DEVNET_PID.to_bytes();
    let sysvar_bytes = solana_program::sysvar::slot_hashes::ID.to_bytes();

    // Copy program ID
    let mut i = 0;
    while i < 32 {
        prefix[i] = pid_bytes[i];
        i += 1;
    }

    // Copy sysvar ID
    i = 0;
    while i < 32 {
        prefix[32 + i] = sysvar_bytes[i];
        i += 1;
    }

    prefix
};

pub struct BundleVerifier<'info, 'a> {
    pub queue: &'a AccountInfo<'info>,
    pub slothash_sysvar: &'a AccountInfo<'info>,
    pub ix_sysvar: &'a AccountInfo<'info>,
    pub max_age: u64,
}

#[derive(Copy, Clone)]
pub struct BundleVerifierBuilder<'info, 'a> {
    queue: *const AccountInfo<'info>,
    slothash_sysvar: *const AccountInfo<'info>,
    ix_sysvar: *const AccountInfo<'info>,
    max_age: u64,
    _phantom: core::marker::PhantomData<(&'a (), &'info ())>,
}

impl<'info, 'a> Default for BundleVerifierBuilder<'info, 'a> {
    fn default() -> Self {
        Self::new()
    }
}

impl<'info, 'a> BundleVerifierBuilder<'info, 'a> {
    #[inline(always)]
    pub fn new() -> Self {
        unsafe { core::mem::zeroed() }
    }

    #[inline(always)]
    pub fn queue<T>(&mut self, account: &'a T) -> &mut Self
    where
        T: AsRef<AccountInfo<'info>>,
    {
        self.queue = account.as_ref() as *const _;
        self
    }

    #[inline(always)]
    pub fn slothash_sysvar<T>(&mut self, sysvar: &'a T) -> &mut Self
    where
        T: AsRef<AccountInfo<'info>>,
    {
        self.slothash_sysvar = sysvar.as_ref() as *const _;
        self
    }

    #[inline(always)]
    pub fn ix_sysvar<T>(&mut self, sysvar: &'a T) -> &mut Self
    where
        T: AsRef<AccountInfo<'info>>,
    {
        self.ix_sysvar = sysvar.as_ref() as *const _;
        self
    }

    #[inline(always)]
    pub fn max_age(&mut self, max_age: u64) -> &mut Self {
        self.max_age = max_age;
        self
    }

    pub fn verify<'instr>(
        &self,
        instruction_data: &'instr [u8],
    ) -> Result<VerifiedBundle<'instr>, AnyError> {
        let queue = unsafe { &*self.queue };
        let slothash_sysvar = unsafe { &*self.slothash_sysvar };

        // Parse ED25519 instruction first to get oracle indices and signed data
        use crate::sysvar::ed25519_sysvar::Ed25519Sysvar;
        let (parsed_sigs, sig_count, oracle_idxs, recent_slot, version) =
            Ed25519Sysvar::parse_instruction_zero_copy(instruction_data)?;

        if sig_count == 0 {
            bail!("No signatures provided");
        }

        // Get queue data for oracle signing keys
        let queue_buf = queue.data.borrow();
        let queue_data: &QueueAccountData = bytemuck::from_bytes(&queue_buf[8..]);

        // Find the target slothash from the bundle
        let reference_sig = &parsed_sigs[0];
        let header = unsafe { reference_sig.bundle_header() };

        // Find the target slothash from bundle and get corresponding hash from sysvar
        let target_slothash = header.signed_slothash;
        let found_slothash = Self::find_slothash_in_sysvar(recent_slot, slothash_sysvar)?;

        // Fixed-size mega validation arrays (max 8 sigs = 64+32+8*32 = 352 bytes max)
        let mut validation_data = [0u8; 352];
        let mut expected_data = [0u8; 352];
        let mut offset = 0;

        // Account keys validation (64 bytes) - use pre-computed expected data
        validation_data[0..32].copy_from_slice(&queue.owner.to_bytes());
        validation_data[32..64].copy_from_slice(&slothash_sysvar.key.to_bytes());
        expected_data[0..64].copy_from_slice(&EXPECTED_PREFIX);
        offset += 64;

        // Slothash validation (32 bytes: found should match target)
        validation_data[offset..offset+32].copy_from_slice(&found_slothash);
        expected_data[offset..offset+32].copy_from_slice(&target_slothash);
        offset += 32;

        // Oracle signing key validation (32 bytes per oracle: actual should match expected)
        for i in 0..sig_count {
            let oracle_idx = (oracle_idxs[i as usize] as usize) % 30;  // Branchless bounds check
            let expected_oracle_key = &queue_data.ed25519_oracle_signing_keys[oracle_idx];
            let actual_oracle_key = unsafe { *parsed_sigs[i as usize].pubkey() };
            validation_data[offset..offset+32].copy_from_slice(&actual_oracle_key);
            expected_data[offset..offset+32].copy_from_slice(&expected_oracle_key.to_bytes());
            offset += 32;
        }

        // Single mega memcmp for all validation
        assert!(solana_program::program_memory::sol_memcmp(
            &validation_data,
            &expected_data,
            offset,
        ) == 0);

        // Continue with remaining processing...
        let reference_feed_infos = unsafe { reference_sig.feed_infos() };
        let feed_count = reference_feed_infos.len();

        Ok(VerifiedBundle::new(
            unsafe { reference_sig.bundle_header() },
            sig_count,
            reference_feed_infos,
            feed_count as u8,
            oracle_idxs,
            recent_slot,
            version,
        ))
    }

    /// Find slothash in sysvar and return it for mega memcmp validation
    fn find_slothash_in_sysvar(
        target_slot: u64,
        slothash_sysvar: &AccountInfo,
    ) -> Result<[u8; 32], AnyError> {
        let slothash_data = slothash_sysvar.data.borrow();
        let slot_data: &[SlotHash] = unsafe { std::mem::transmute(&slothash_data[8..]) };

        // Use clock hint for faster search
        let mut estimated_idx = ((slot_data[0].slot - target_slot) % SYSVAR_SLOT_LEN) as usize;

        // Optimized search with early termination
        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");
    }

}

/// Convenience function for extracting ED25519 instruction from sysvar
/// Handles the type coercion from Anchor's Sysvar wrapper to AccountInfo
#[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())
}