Verify report data - Offchain integration (Solana)

Guide Versions

This guide is available in multiple versions. Choose the one that matches your needs.

To verify a Data Streams report, you must confirm the report integrity signed by the Decentralized Oracle Network (DON).

You have two options to verify Data Streams reports on Solana:

  1. Onchain integration: Verify reports directly within your Solana program using Cross-Program Invocation (CPI) to the verifier program. Learn more about this method in the onchain verification guide.

  2. Offchain integration: Verify reports client-side using an SDK. You'll learn how to implement this method in this guide.

Both methods use the same underlying verification logic and security guarantees, differing only in where the verification occurs.

Offchain integration

The offchain integration allows you to verify the authenticity of Data Streams reports from your client-side application. While this method requires sending a transaction to the verifier program, the verification logic and processing of results happens in your client application rather than in a Solana program.

In this guide, you'll learn how to:

  • Set up an Anchor project
  • Configure the necessary dependencies
  • Create a command-line tool to verify reports
  • Process and display the verified report data

Prerequisites

Before you begin, you should have:

Requirements

To complete this guide, you'll need:

  • Rust and Cargo: Install the latest version using rustup. Run rustc --version to verify your installation.

  • Solana CLI tools: Install the latest version following the official guide. Run solana --version to verify your installation.

  • Anchor Framework: Follow the official installation guide. Run anchor --version to verify your installation.

  • Devnet SOL: You'll need devnet SOL for transaction fees. Use the Solana CLI or the Solana Faucet to get devnet SOL. Check your balance with solana balance.

  • Allowlisted Account: Your account must be allowlisted in the Data Streams Access Controller.

Implementation guide

1. Create a new Anchor project

  1. Create a new Anchor project:

    anchor init example_verify
    cd example_verify
    
  2. Create a binary target for the verification tool:

    mkdir -p programs/example_verify/src/bin
    touch programs/example_verify/src/bin/main.rs
    

2. Configure your project's dependencies

Update your program's manifest file (programs/example_verify/Cargo.toml):

[package]
name = "example_verify"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]

[[bin]]
name = "example_verify"
path = "src/bin/main.rs"

[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []

[dependencies]
chainlink-data-streams-report = "1.0.0"
sdk-off-chain = { git = "https://github.com/smartcontractkit/smart-contract-examples.git", branch = "data-streams-solana-integration", package = "sdk-off-chain" }

solana-program = "1.18.26"
solana-sdk = "1.18.26"
solana-client = "1.18.26"
solana-transaction-status = "1.18.26"
hex = "0.4.3"
borsh = "0.10.3"
base64 = "0.22.1"
snap = "1.1.1"

3. Implement the verification library

Create programs/example_verify/src/lib.rs with the verification function:

use chainlink_data_streams_report::report::v3::ReportDataV3;
use sdk_off_chain::{ VerificationClient, VerificationResult };
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
    commitment_config::CommitmentConfig,
    pubkey::Pubkey,
    signature::{ read_keypair_file, Keypair },
    signer::Signer,
};
use std::{ path::PathBuf, str::FromStr, fs::File, io::Read };

pub fn default_keypair_path() -> String {
    let mut path = PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()));
    path.push(".config/solana/id.json");
    path.to_str().unwrap().to_string()
}

pub fn load_keypair(keypair_path: Option<&str>) -> Result<Keypair, Box<dyn std::error::Error>> {
    let path = keypair_path.map(|p| p.to_string()).unwrap_or_else(default_keypair_path);

    // Try reading as a file-based keypair first
    match read_keypair_file(&path) {
        Ok(keypair) => Ok(keypair),
        Err(_) => {
            // If file read fails, try as a JSON string
            let mut file = File::open(&path)?;
            let mut contents = String::new();
            file.read_to_string(&mut contents)?;

            let keypair = Keypair::from_base58_string(&contents);
            Ok(keypair)
        }
    }
}

pub fn verify_report(
    signed_report: &[u8],
    program_id: &str,
    access_controller: &str,
    keypair_path: Option<&str>,
    rpc_url: Option<&str>
) -> Result<ReportDataV3, Box<dyn std::error::Error>> {
    // Initialize RPC client with confirmed commitment level
    let rpc_url = rpc_url.unwrap_or("https://api.devnet.solana.com");
    let rpc_client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());

    // Load the keypair that will pay for and sign verification transactions
    let payer = load_keypair(keypair_path)?;
    println!("Using keypair: {}", payer.pubkey());

    // Convert to Pubkey
    let program_pubkey = Pubkey::from_str(program_id)?;
    let access_controller_pubkey = Pubkey::from_str(access_controller)?;
    println!("Program ID: {}", program_pubkey);
    println!("Access Controller: {}", access_controller_pubkey);

    // Create a verification client instance
    let client = VerificationClient::new(
        program_pubkey,
        access_controller_pubkey,
        rpc_client,
        payer
    );

    // Verify the report
    println!("Verifying report of {} bytes...", signed_report.len());
    let result = verify_and_handle_errors(&client, signed_report)?;

    // Decode the returned data into a ReportDataV3 struct
    let return_data = result.return_data.ok_or_else(|| {
        Box::<dyn std::error::Error>::from("No return data from verification")
    })?;

    let report = ReportDataV3::decode(&return_data)?;
    Ok(report)
}

// Helper function to handle the verification process and errors
fn verify_and_handle_errors(
    client: &VerificationClient,
    signed_report: &[u8]
) -> Result<VerificationResult, Box<dyn std::error::Error>> {
    match client.verify(signed_report.to_vec()) {
        Ok(result) => Ok(result),
        Err(e) => {
            println!("Verification error: {:?}", e);

            // Convert ClientError to a boxed Error
            Err(Box::new(e))
        }
    }
}

4. Create the command-line interface

Create programs/example_verify/src/bin/main.rs:

use example_verify::verify_report;
use std::env;
use std::str::FromStr;
use hex;
use solana_sdk::pubkey::Pubkey;

fn main() {
    let args: Vec<String> = env::args().collect();

    // Check if help flag is passed
    if args.len() > 1 && (args[1] == "-h" || args[1] == "--help") {
        print_usage(&args[0]);
        return;
    }

    if args.len() < 4 {
        eprintln!("Error: Not enough arguments provided.");
        print_usage(&args[0]);
        std::process::exit(1);
    }

    let program_id_str = &args[1];
    let access_controller_str = &args[2];
    let hex_report = &args[3];

    // Optional arguments
    let keypair_path = args.get(4).map(|s| s.as_str());
    let rpc_url = args.get(5).map(|s| s.as_str());

    // Validate program_id and access_controller
    if Pubkey::from_str(program_id_str).is_err() {
        eprintln!("Invalid program ID provided");
        std::process::exit(1);
    }
    if Pubkey::from_str(access_controller_str).is_err() {
        eprintln!("Invalid access controller address provided");
        std::process::exit(1);
    }

    // Decode the hex string for the signed report
    let signed_report = match hex::decode(hex_report) {
        Ok(bytes) => bytes,
        Err(e) => {
            eprintln!("Failed to decode hex string: {}", e);
            std::process::exit(1);
        }
    };

    // Perform verification off-chain
    match
        verify_report(&signed_report, program_id_str, access_controller_str, keypair_path, rpc_url)
    {
        Ok(report) => {
            println!("\nVerified Report Data:");
            println!("Feed ID: {}", report.feed_id);
            println!("Valid from timestamp: {}", report.valid_from_timestamp);
            println!("Observations timestamp: {}", report.observations_timestamp);
            println!("Native fee: {}", report.native_fee);
            println!("Link fee: {}", report.link_fee);
            println!("Expires at: {}", report.expires_at);
            println!("Benchmark price: {}", report.benchmark_price);
            println!("Bid: {}", report.bid);
            println!("Ask: {}", report.ask);
        }
        Err(e) => {
            eprintln!("Verification failed: {}", e);
            std::process::exit(1);
        }
    }
}

fn print_usage(program_name: &str) {
    println!("Usage: {} <program-id> <access-controller> <hex-encoded-signed-report> [keypair-path] [rpc-url]", program_name);
    println!("Arguments:");
    println!("  program-id                 The Solana program ID for the verification");
    println!("  access-controller          The access controller public key");
    println!("  hex-encoded-signed-report  The signed report data in hex format");
    println!(
        "  keypair-path               Optional: Path to the keypair file (default: ~/.config/solana/id.json)"
    );
    println!(
        "  rpc-url                    Optional: URL for the Solana RPC endpoint (default: https://api.devnet.solana.com)"
    );
}

5. Build and run the verifier

  1. Build the project:

    cargo build
    
  2. Make sure you are connected to Devnet with solana config set --url https://api.devnet.solana.com.

  3. Run the verifier with your report:

    cargo run -- <program-id> <access-controller> <hex-encoded-signed-report>
    

    Replace the placeholders with:

    • <program-id>: The Verifier Program ID (find it on the Stream Addresses page)
    • <access-controller>: The Access Controller Account (find it on the Stream Addresses page)
    • <hex-encoded-signed-report>: Your hex-encoded signed report (without the '0x' prefix)

    Example:

    cargo run -- Gt9S41PtjR58CbG9JhJ3J6vxesqrNAswbWYbLNTMZA3c 2k3DsgwBoqrnvXKVvd7jX7aptNxdcRBdcd5HkYsGgbrb 0006f9b553e393ced311551efd30d1decedb63d76ad41737462e2cdbbdff1578000000000000000000000000000000000000000000000000000000004f56930f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000028001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000120000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba78200000000000000000000000000000000000000000000000000000000675ca37000000000000000000000000000000000000000000000000000000000675ca3700000000000000000000000000000000000000000000000000000174be1bd8758000000000000000000000000000000000000000000000000000cb326ce8c3ea800000000000000000000000000000000000000000000000000000000675df4f00000000000000000000000000000000000000000000000d3a30bcc15e207c0000000000000000000000000000000000000000000000000d3a1557b5e634060200000000000000000000000000000000000000000000000d3ab99a974ff10f400000000000000000000000000000000000000000000000000000000000000000292bdd75612560e46ed9b0c2437898f81eb0e18b6b902a161b9708e9177175cf3b8ef2b279f230f766fb29306250ee90856516ee349ca42b2d7fb141deb006745000000000000000000000000000000000000000000000000000000000000000221c156e80276827e1bfeb6542ab064dfa958f5be955f516fb62b1c93437472c31cc65fcaba68c9d661701190bc32025a0690af0eefe027ac218fd15c588dd4d5
    

    Expect the output to be similar to the following:

    Using keypair: <YOUR_PUBLIC_KEY>
    Program ID: Gt9S41PtjR58CbG9JhJ3J6vxesqrNAswbWYbLNTMZA3c
    Access Controller: 2k3DsgwBoqrnvXKVvd7jX7aptNxdcRBdcd5HkYsGgbrb
    Verifying report of 736 bytes...
    
    Verified Report Data:
    Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
    Valid from timestamp: 1734124400
    Observations timestamp: 1734124400
    Native fee: 25614677280600
    Link fee: 3574678975954600
    Expires at: 1734210800
    Benchmark price: 3904011708000000000000
    Bid: 3903888333211164500000
    

Best practices

When implementing verification in production:

  1. Error Handling:

    • Implement robust error handling for network issues
    • Add proper logging and monitoring
    • Handle report expiration gracefully
  2. Security:

    • Securely manage keypairs and never expose them
    • Validate all input parameters
    • Implement rate limiting for verification requests
  3. Performance:

    • Cache verified reports when appropriate
    • Implement retry mechanisms with backoff
    • Use connection pooling for RPC clients

What's next

Get the latest Chainlink content straight to your inbox.