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:
- Solana Program emits an event like
UpdateRequired
when it needs fresh oracle data.
- Off-chain service (keeper, bot, backend) listens to that event.
- The service fetches signed price data from the Chaos Oracle API.
- It verifies the signature off-chain using the verifier module.
- 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:
- Import the module into your project.
- 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)
- Call
verify_signature_from_encoded()
with the inputs to check authenticity.
- 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.
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:
- Reconstructing the canonical message hash
- Performing secp256k1 signature recovery
- 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]