Skip to main content
This module provides a fully off-chain implementation of Chaos oracle signature verification. It’s designed for use in:
  • Backend verification pipelines
  • Indexers and data relayers
  • Bots and off-chain keepers
  • Audit and forensic tools
The verification logic replicates Solana’s on-chain behavior, allowing external systems to validate oracle-signed messages before passing them to Solana programs or end-users.

How it Works

The diagram below illustrates how off-chain services interact with Chaos Oracle data and Solana programs:
  1. Solana Program emits an event like UpdateRequired when it needs fresh oracle data.
  2. Off-chain service (keeper, bot, backend) listens to that event.
  3. The service fetches signed price data from the Chaos Oracle API.
  4. It verifies the signature off-chain using the verifier module.
  5. If valid, it submits the update back on-chain via a program instruction.
This method can be used to filter invalid data before writing to disk, emitting events, or forwarding to Solana programs.

Deployment Notes

To use the off-chain verification module in your Rust backend or CLI tool:
  1. Import the module into your project.
  2. Gather the following inputs:
    • Oracle-signed PriceData (includes feed ID, price, exponent, and timestamp)
    • The 64-byte signature, encoded as a hex string
    • A recovery ID (an integer from 0 to 3)
    • The oracle’s public key (compressed, base64-encoded)
  3. Call verify_signature_from_encoded() with the inputs to check authenticity.
  4. Accept only prices that return Ok(true) to ensure data integrity and prevent tampered or stale data from being used.
Here’s how to use the helper function:
The following code is for illustrative purposes only, has not been audited, and should not be used in production without thorough testing.
RUST
use verifier::{PriceData, verify_signature_from_encoded};

let prices = vec![PriceData {
    feed_id: "BTCUSD".to_string(),
    price: 6500000000000,
    ts: 1678886400,
    expo: -8,
}];

let is_valid = verify_signature_from_encoded(
    &prices,
    "b4ab1a...bff2", // Hex-encoded signature
    0,              // Recovery ID
    "A3Xv0K...=="   // Base64-encoded public key
)?;

if is_valid {
    println!("✅ Signature is valid");
} else {
    println!("❌ Signature is invalid");
}

Batch Price Verification

use solana_program::{
    keccak::hashv,
    secp256k1_recover::{secp256k1_recover, Secp256k1RecoverError},
};
use base64::prelude::*;
use anyhow::{Result, anyhow};
use hex;

/// Price data structure representing a single price entry from the oracle
#[derive(Debug, Clone)]
pub struct PriceData {
    pub feed_id: String,  // Identifier for the price feed such as "_BTCUSD_"
    pub price: u64,       // Price value (scaled by 10^expo)
    pub ts: u64,          // Timestamp in seconds
    pub expo: i8,         // Exponent like -8 means divide price by 10^8
}

/// Build a message hash from price data entries
/// This creates a deterministic hash that matches what was signed by Chaos
pub fn build_message_hash(prices: &[PriceData]) -> Result<[u8; 32]> {
    // Create buffers for each price entry
    let mut buffers = Vec::with_capacity(prices.len());
    
    for price in prices {
        // Format each price entry:
        // 1. feed_id (padded to 32 bytes)
        // 2. price (8 bytes, little-endian)
        // 3. expo (1 byte)
        // 4. timestamp (8 bytes, little-endian)
        
        let mut msg = vec![0u8; 32]; // Start with 32 zero bytes
        let feed_bytes = price.feed_id.as_bytes();
        let copy_len = std::cmp::min(feed_bytes.len(), 32);
        msg[..copy_len].copy_from_slice(&feed_bytes[..copy_len]);
        
        // Append price as 8-byte little-endian
        msg.extend_from_slice(&price.price.to_le_bytes());
        
        // Append expo as a single byte
        msg.push(price.expo as u8);
        
        // Append timestamp as 8-byte little-endian
        msg.extend_from_slice(&price.ts.to_le_bytes());
        
        buffers.push(msg);
    }
    
    // Use Solana's hashv function to hash all price entries together,
    let buffer_refs: Vec<&[u8]> = buffers.iter().map(|buf| buf.as_slice()).collect();
    let hash = hashv(&buffer_refs);
    
    Ok(hash.to_bytes())
}

/// Verify a signature using Solana's native secp256k1 recovery function
/// 
/// Parameters:
/// - message_hash: The 32-byte hash of the message that was signed
/// - signature: The 64-byte signature (r,s) components
/// - recovery_id: The recovery ID (0 or 1)
/// - expected_public_key: The public key that should have created the signature
///
/// Returns:
/// - Ok(true) if signature is valid
/// - Ok(false) if signature is invalid
/// - Err if there was an error during verification
pub fn verify_signature(
    message_hash: &[u8; 32],
    signature: &[u8; 64],
    recovery_id: u8,
    expected_public_key: &[u8],
) -> Result<bool> {
    // Use Solana's secp256k1_recover function to recover the public key from the signature
    let recovered_pubkey = secp256k1_recover(
        message_hash,
        recovery_id,
        signature,
    ).map_err(|e| {
        match e {
            Secp256k1RecoverError::InvalidRecoveryId => anyhow!("Invalid recovery ID"),
            Secp256k1RecoverError::InvalidSignature => anyhow!("Invalid signature format"),
            _ => anyhow!("Signature recovery failed"),
        }
    })?;
    
    // Handle compressed public keys (33 bytes)
    if expected_public_key.len() == 33 {
        // Parse both keys for comparison
        let recovered_key = libsecp256k1::PublicKey::parse_slice(
            &recovered_pubkey.to_bytes(),
            None
        ).map_err(|_| anyhow!("Failed to parse recovered public key"))?;
        
        let expected_key = libsecp256k1::PublicKey::parse_slice(
            expected_public_key,
            None
        ).map_err(|_| anyhow!("Failed to parse expected public key"))?;
        
        // Compare serialized keys
        return Ok(recovered_key.serialize() == expected_key.serialize());
    }
    
    // Handle uncompressed public keys (65 bytes)
    if expected_public_key.len() == 65 {
        // Compare directly with recovered key
        return Ok(recovered_pubkey.to_bytes() == expected_public_key);
    }
    
    // Unsupported key format
    Err(anyhow!("Unsupported public key format (length: {})", expected_public_key.len()))
}

/// Convenience function to verify a signature from encoded inputs
/// 
/// Parameters:
/// - prices: Array of price data entries
/// - signature_hex: Hex-encoded signature string
/// - recovery_id: Recovery ID (0 or 1)
/// - public_key_base64: Base64-encoded public key
///
/// Returns:
/// - Ok(true) if signature is valid
/// - Ok(false) if signature is invalid
/// - Err if there was an error during verification
pub fn verify_signature_from_encoded(
    prices: &[PriceData],
    signature_hex: &str,
    recovery_id: u8,
    public_key_base64: &str
) -> Result<bool> {
    // 1. Build the message hash
    let message_hash = build_message_hash(prices)?;
    println!("Message hash: {}", hex::encode(&message_hash));
    
    // 2. Decode the signature from hex
    let signature_bytes = hex::decode(signature_hex)
        .map_err(|_| anyhow!("Failed to decode signature from hex"))?;
    
    if signature_bytes.len() != 64 {
        return Err(anyhow!("Invalid signature length ({} bytes)", signature_bytes.len()));
    }
    
    let mut signature = [0u8; 64];
    signature.copy_from_slice(&signature_bytes);
    
    // 3. Decode the public key from base64
    let public_key = BASE64_STANDARD.decode(public_key_base64.as_bytes())
        .map_err(|_| anyhow!("Failed to decode public key from base64"))?;
    
    println!("Public key length: {} bytes", public_key.len());
    
    // 4. Verify the signature
    verify_signature(&message_hash, &signature, recovery_id, &public_key)
}

What the Code Does

The Rust module verifies that signed price messages originated from a trusted oracle by:
  1. Reconstructing the canonical message hash
  2. Performing secp256k1 signature recovery
  3. Comparing the recovered public key to an allowlisted oracle signer
This verification ensures the data hasn’t been tampered with before being accepted by your backend or forwarded to your Solana programs.

Key Features:

  • Constructs a Keccak-256 message hash from multiple PriceData entries
  • Supports both compressed (33-byte) and uncompressed (65-byte) public keys
  • Verifies signatures using Solana’s native secp256k1_recover function
  • Includes convenience wrappers to decode hex/base64 inputs

Support

For assistance with implementing your off-chain service, contact our support team at [email protected]
I