This tutorial demonstrates how to verify oracle feed configurations on-chain using Kalshi prediction market data. You'll learn a critical security pattern that prevents oracle substitution attacks.
By recreating the expected feed configuration on-chain and comparing its hash to the oracle's feed ID, you cryptographically prove the oracle uses exactly the data sources you expect.
What You'll Build
A Solana program that:
Receives oracle data for a Kalshi prediction market order
Recreates the expected feed configuration on-chain
Verifies the feed ID matches before trusting the data
Prerequisites
Rust and Cargo installed
Anchor framework familiarity
Solana CLI installed and configured
Kalshi API credentials (API key ID and private key)
Key Concepts
Feed ID Derivation
Feed IDs are derived by:
Constructing an OracleFeed protobuf message
Encoding it as length-delimited bytes
Computing SHA-256 hash
QuoteVerifier
The QuoteVerifier uses a builder pattern to verify Ed25519 signatures from oracle operators:
Variable Overrides
Kalshi requires authentication. Variables like ${KALSHI_API_KEY_ID} are placeholders that get replaced at runtime when fetching the quote:
The On-Chain Program
Dependencies
Note: The current example program uses switchboard-on-demand 0.13.0 with the anchor and devnet features for the anchor-lang 0.31.1 toolchain.
Program Structure
Feed ID Recreation
The critical function that recreates the expected feed configuration:
Account Context
The TypeScript Client
Kalshi Authentication
Kalshi uses RSA-PSS-SHA256 signatures for API authentication:
[dependencies]
anchor-lang = "0.31.1"
switchboard-on-demand = { version = "0.13.0", features = ["anchor", "devnet"] }
switchboard-protos = { version = "0.2.6", features = ["serde"] }
prost = "0.13"
solana-program = ">=2,<3"
faster-hex = "0.10.0"
use anchor_lang::prelude::*;
use switchboard_on_demand::{SlotHashes, Instructions, QuoteVerifier};
use switchboard_protos::OracleFeed;
use switchboard_protos::OracleJob;
use switchboard_protos::oracle_job::oracle_job::{KalshiApiTask, JsonParseTask, Task};
use switchboard_protos::oracle_job::oracle_job::task;
use switchboard_on_demand::QueueAccountData;
use switchboard_on_demand::default_queue;
use prost::Message;
use solana_program::hash::hash;
declare_id!("YOUR_PROGRAM_ID");
#[program]
pub mod prediction_market {
use super::*;
pub fn verify_kalshi_feed(
ctx: Context<VerifyFeed>,
order_id: String,
) -> Result<()> {
// Step 1: Create QuoteVerifier with builder pattern
let mut verifier = QuoteVerifier::new();
verifier
.queue(ctx.accounts.queue.as_ref())
.slothash_sysvar(ctx.accounts.slothashes.as_ref())
.ix_sysvar(ctx.accounts.instructions.as_ref())
.clock_slot(Clock::get()?.slot);
// Step 2: Verify the Ed25519 instruction at index 0
let quote = verifier.verify_instruction_at(0).unwrap();
// Step 3: Extract feed ID from verified quote
let feeds = quote.feeds();
require!(!feeds.is_empty(), ErrorCode::NoOracleFeeds);
let feed = &feeds[0];
let actual_feed_id = feed.feed_id();
// Step 4: Recreate expected feed ID and verify match
require!(
*actual_feed_id == create_kalshi_feed_id(&order_id)?,
ErrorCode::FeedMismatch
);
msg!("Feed ID verification successful!");
msg!("Feed ID: {}", faster_hex::hex_string(actual_feed_id));
msg!("Order ID: {}", order_id);
Ok(())
}
}
fn create_kalshi_feed_id(order_id: &str) -> Result<[u8; 32]> {
// Build the Kalshi API URL
let url = format!(
"https://api.elections.kalshi.com/trade-api/v2/portfolio/orders/{}",
order_id
);
// Construct the exact feed definition
let feed = OracleFeed {
name: Some("Kalshi Order Price".to_string()),
jobs: vec![
OracleJob {
tasks: vec![
// Task 1: Fetch from Kalshi API
Task {
task: Some(task::Task::KalshiApiTask(KalshiApiTask {
url: Some(url.clone()),
api_key_id: Some("${KALSHI_API_KEY_ID}".to_string()),
signature: Some("${KALSHI_SIGNATURE}".to_string()),
timestamp: Some("${KALSHI_TIMESTAMP}".to_string()),
..Default::default()
})),
},
// Task 2: Parse JSON response
Task {
task: Some(task::Task::JsonParseTask(JsonParseTask {
path: Some("$.order.yes_price_dollars".to_string()),
..Default::default()
})),
},
],
weight: None,
}
],
min_job_responses: Some(1), // unscaled job/source quorum
min_oracle_samples: Some(1), // unscaled oracle/signature quorum
max_job_range_pct: Some(0), // Intentional for this single-source verification; use a positive scaled value for normal multi-source feeds.
};
// Encode as protobuf and hash
let bytes = OracleFeed::encode_length_delimited_to_vec(&feed);
Ok(hash(&bytes).to_bytes())
}
#[derive(Accounts)]
pub struct VerifyFeed<'info> {
/// The Switchboard queue - must be the default queue
#[account(address = default_queue())]
pub queue: AccountLoader<'info, QueueAccountData>,
/// SlotHashes sysvar for signature verification
pub slothashes: Sysvar<'info, SlotHashes>,
/// Instructions sysvar for Ed25519 verification
pub instructions: Sysvar<'info, Instructions>,
}
#[error_code]
pub enum ErrorCode {
#[msg("No oracle feeds available")]
NoOracleFeeds,
#[msg("Feed hash mismatch - oracle feed does not match expected configuration")]
FeedMismatch,
}
// Verify feed configuration before settlement
verify_kalshi_feed(ctx, order_id)?;
// Now safe to use the oracle value
let price = feeds[0].value();
settle_positions(price)?;
verify_kalshi_feed(ctx, order_id)?;
let yes_price = feeds[0].value();
if yes_price > threshold {
release_funds()?;
}