Verify report data - Onchain 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. You'll learn how to implement this method in this guide.

  2. Offchain integration: Verify reports client-side using an SDK. Learn more about this method in the offchain verification guide.

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

Onchain integration

You can verify Data Streams reports directly within your Solana program using this integration. This method ensures atomic verification and processing of report data.

In this guide, you'll learn how to:

  • Integrate with the Chainlink Data Streams Verifier program
  • Create and invoke the verification instruction
  • Retrieve the verified report data

Prerequisites

Before you begin, you should have:

Requirements

To complete this guide, you'll need:

  • Rust tool-chain (rustup): Use rustc and cargo โ‰ฅ 1.73 (the version Anchor 0.31 requires). Run rustc --version to verify your installation.

  • Solana CLI tools: Use 2.x (Anchor 0.31 is built against the 2.x SDK). Install an appropriate version following the official guide. Run solana --version to verify your installation.

  • Anchor Framework: Use Anchor โ‰ฅ 0.31.0 (older 0.29/0.30 will not work with Solana 2.x.). Follow the official installation guide. Run anchor --version to verify your installation.

  • Node.js and npm: Install Node.js โ‰ฅ 20. Verify your installation with node --version.

  • ts-node: Use ts-node โ‰ฅ 10. Install globally using npm: npm install -g ts-node. Verify your installation with ts-node --version.

  • Devnet SOL: You'll need ~3-5 SOL in your devnet wallet for deployment and testing. Use the Solana CLI or the Solana Faucet to get devnet SOL.

Implementation guide

1. Create a new Anchor project

  1. Open your terminal and run the following command to create a new Anchor project:

    anchor init example_verify
    

    This command creates a new directory named example_verify with the basic structure of an Anchor project.

  2. Navigate to the project directory:

    cd example_verify
    

2. Configure your project for devnet

Open your Anchor.toml file at the root of your project and update it to use devnet:

[toolchain]
solana_version = "2.1.0"

[features]
resolution = true
skip-lint = false

[programs.devnet]
example_verify = "<YOUR_PROGRAM_ID>"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "devnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

Replace <YOUR_PROGRAM_ID> with your program ID. You can run solana-keygen pubkey target/deploy/example_verify-keypair.json to get your program ID.

3. Set up your program's dependencies

In your program's manifest file (programs/example_verify/Cargo.toml), add the Chainlink Data Streams client and the report crate as dependencies:

[dependencies]
chainlink_solana_data_streams = { git = "https://github.com/smartcontractkit/chainlink-data-streams-solana", subdir = "crates/chainlink-solana-data-streams", tag = "v1.1.0" }
chainlink-data-streams-report = "1.0.0"

# Additional required dependencies
anchor-lang = { version = "0.31.0", features = ["idl-build"] }

4. Write the program

Navigate to your program main file (programs/example_verify/src/lib.rs). This is where you'll write your program logic. Replace the contents of lib.rs with the following example code:

// Import required dependencies for Anchor, Solana, and Data Streams
use anchor_lang::prelude::*;
use anchor_lang::solana_program::{
    instruction::Instruction,
    program::{get_return_data, invoke},
    pubkey::Pubkey,
};
use chainlink_data_streams_report::report::v3::ReportDataV3;
use chainlink_solana_data_streams::VerifierInstructions;

declare_id!("<YOUR_PROGRAM_ID>");

#[program]
pub mod example_verify {
    use super::*;

    /// Verifies a Data Streams report using Cross-Program Invocation to the Verifier program
    /// Returns the decoded report data if verification succeeds
    pub fn verify(ctx: Context<ExampleProgramContext>, signed_report: Vec<u8>) -> Result<()> {
        let program_id = ctx.accounts.verifier_program_id.key();
        let verifier_account = ctx.accounts.verifier_account.key();
        let access_controller = ctx.accounts.access_controller.key();
        let user = ctx.accounts.user.key();
        let config_account = ctx.accounts.config_account.key();

        // Create verification instruction
        let chainlink_ix: Instruction = VerifierInstructions::verify(
            &program_id,
            &verifier_account,
            &access_controller,
            &user,
            &config_account,
            signed_report,
        );

        // Invoke the Verifier program
        invoke(
            &chainlink_ix,
            &[
                ctx.accounts.verifier_account.to_account_info(),
                ctx.accounts.access_controller.to_account_info(),
                ctx.accounts.user.to_account_info(),
                ctx.accounts.config_account.to_account_info(),
            ],
        )?;

        // Decode and log the verified report data
        if let Some((_program_id, return_data)) = get_return_data() {
            msg!("Report data found");
            let report = ReportDataV3::decode(&return_data)
                .map_err(|_| error!(CustomError::InvalidReportData))?;

            // Log report fields
            msg!("FeedId: {}", report.feed_id);
            msg!("Valid from timestamp: {}", report.valid_from_timestamp);
            msg!("Observations Timestamp: {}", report.observations_timestamp);
            msg!("Native Fee: {}", report.native_fee);
            msg!("Link Fee: {}", report.link_fee);
            msg!("Expires At: {}", report.expires_at);
            msg!("Benchmark Price: {}", report.benchmark_price);
            msg!("Bid: {}", report.bid);
            msg!("Ask: {}", report.ask);
        } else {
            msg!("No report data found");
            return Err(error!(CustomError::NoReportData));
        }

        Ok(())
    }
}

#[error_code]
pub enum CustomError {
    #[msg("No valid report data found")]
    NoReportData,
    #[msg("Invalid report data format")]
    InvalidReportData,
}

#[derive(Accounts)]
pub struct ExampleProgramContext<'info> {
    /// The Verifier Account stores the DON's public keys and other verification parameters.
    /// This account must match the PDA derived from the verifier program.
    /// CHECK: The account is validated by the verifier program.
    pub verifier_account: AccountInfo<'info>,
    /// The Access Controller Account
    /// CHECK: The account structure is validated by the verifier program.
    pub access_controller: AccountInfo<'info>,
    /// The account that signs the transaction.
    pub user: Signer<'info>,
    /// The Config Account is a PDA derived from a signed report
    /// CHECK: The account is validated by the verifier program.
    pub config_account: UncheckedAccount<'info>,
    /// The Verifier Program ID specifies the target Chainlink Data Streams Verifier Program.
    /// CHECK: The program ID is validated by the verifier program.
    pub verifier_program_id: AccountInfo<'info>,
}

Replace <YOUR_PROGRAM_ID> with your program ID in the declare_id! macro. You can run solana-keygen pubkey target/deploy/example_verify-keypair.json to get your program ID.

Note how the VerifierInstructions::verify helper method automatically handles the PDA computations internally. Refer to the Program Derived Addresses (PDAs) section for more information.

5. Deploy your program

  1. Run the following command to build your program:

    anchor build
    
  2. Deploy your program to a Solana cluster (devnet in this example) using:

    anchor deploy
    

    Expect an output similar to the following:

    Deploying cluster: https://api.devnet.solana.com
    Upgrade authority: ~/.config/solana/id.json
    Deploying program "example_verify"...
    Program path: ~/example_verify/target/deploy/example_verify.so...
    Program Id: ET4wvk16BozbiAyja89uHPGZxXzosyBFWcY6E6koCHxF
    
    Signature: FoFxJyWEYzwiA8k8W4mh4TC8EJicbvXKWjha2SGpD6EggXJWBRpKoHhiAtsuWJgN9pPDfyrfa92qCKmmPkvt9Ub
    
    Deploy success
    

6. Interact with the Verifier Program

In this section, you'll write a client script to interact with your deployed program, which will use Cross-Program Invocation (CPI) to verify reports through the Chainlink Data Streams Verifier Program.

  1. In the tests directory, create a new file verify_test.ts to interact with your deployed program.

  2. Populate your verify_test.ts file with the example client script below. This example provides a report payload. To use your own report payload, update the hexString variable.

    import * as anchor from "@coral-xyz/anchor"
    import { Program } from "@coral-xyz/anchor"
    import { PublicKey } from "@solana/web3.js"
    import { ExampleVerify } from "../target/types/example_verify"
    import * as snappy from "snappy"
    
    // Data Streams Verifier Program ID on Devnet
    const VERIFIER_PROGRAM_ID = new PublicKey("Gt9S41PtjR58CbG9JhJ3J6vxesqrNAswbWYbLNTMZA3c")
    
    async function main() {
      // Setup connection and provider
      const provider = anchor.AnchorProvider.env()
      anchor.setProvider(provider)
    
      // Initialize the program using Anchor workspace - this automatically
      // loads the IDL and program ID from lib.rs (declare_id!)
      const program = anchor.workspace.ExampleVerify as Program<ExampleVerify>
    
      // Convert the hex string to a Uint8Array
      // This is an example report payload for a crypto stream
      const hexString =
        "0x00064f2cd1be62b7496ad4897b984db99243e0921906f66ded15149d993ef42c000000000000000000000000000000000000000000000000000000000103c90c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001200003684ea93c43ed7bd00ab3bb189bb62f880436589f1ca58b599cd97d6007fb0000000000000000000000000000000000000000000000000000000067570fa40000000000000000000000000000000000000000000000000000000067570fa400000000000000000000000000000000000000000000000000004c6ac85bf854000000000000000000000000000000000000000000000000002e1bf13b772a9c0000000000000000000000000000000000000000000000000000000067586124000000000000000000000000000000000000000000000000002bb4cf7662949c000000000000000000000000000000000000000000000000002bae04e2661000000000000000000000000000000000000000000000000000002bb6a26c3fbeb80000000000000000000000000000000000000000000000000000000000000002af5e1b45dd8c84b12b4b58651ff4173ad7ca3f5d7f5374f077f71cce020fca787124749ce727634833d6ca67724fd912535c5da0f42fa525f46942492458f2c2000000000000000000000000000000000000000000000000000000000000000204e0bfa6e82373ae7dff01a305b72f1debe0b1f942a3af01bad18e0dc78a599f10bc40c2474b4059d43a591b75bdfdd80aafeffddfd66d0395cca2fdeba1673d"
    
      // Remove the '0x' prefix if present
      const cleanHexString = hexString.startsWith("0x") ? hexString.slice(2) : hexString
    
      // Validate hex string format
      if (!/^[0-9a-fA-F]+$/.test(cleanHexString)) {
        throw new Error("Invalid hex string format")
      }
    
      // Convert hex to Uint8Array
      const signedReport = new Uint8Array(cleanHexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)))
    
      // Compress the report using Snappy
      const compressedReport = await snappy.compress(Buffer.from(signedReport))
    
      // Derive necessary PDAs using the SDK's helper functions
      const verifierAccount = await PublicKey.findProgramAddressSync([Buffer.from("verifier")], VERIFIER_PROGRAM_ID)
    
      const configAccount = await PublicKey.findProgramAddressSync([signedReport.slice(0, 32)], VERIFIER_PROGRAM_ID)
    
      // The Data Streams access controller on devnet
      const accessController = new PublicKey("2k3DsgwBoqrnvXKVvd7jX7aptNxdcRBdcd5HkYsGgbrb")
    
      try {
        console.log("\n๐Ÿ“ Transaction Details")
        console.log("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”")
        console.log("๐Ÿ”‘ Signer:", provider.wallet.publicKey.toString())
    
        const tx = await program.methods
          .verify(compressedReport)
          .accounts({
            verifierAccount: verifierAccount[0],
            accessController: accessController,
            user: provider.wallet.publicKey,
            configAccount: configAccount[0],
            verifierProgramId: VERIFIER_PROGRAM_ID,
          })
          .rpc({ commitment: "confirmed" })
    
        console.log("โœ… Transaction successful!")
        console.log("๐Ÿ”— Signature:", tx)
    
        // Fetch and display logs
        const txDetails = await provider.connection.getTransaction(tx, {
          commitment: "confirmed",
          maxSupportedTransactionVersion: 0,
        })
    
        console.log("\n๐Ÿ“‹ Program Logs")
        console.log("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”")
    
        let indentLevel = 0
        let currentProgramId = ""
        txDetails.meta.logMessages.forEach((log) => {
          // Handle indentation for inner program calls
          if (log.includes("Program invoke")) {
            const programIdMatch = log.match(/Program (.*?) invoke/)
            if (programIdMatch) {
              currentProgramId = programIdMatch[1]
              // Remove "Unknown Program" prefix if present
              currentProgramId = currentProgramId.replace("Unknown Program ", "")
              // Remove parentheses if present
              currentProgramId = currentProgramId.replace(/[()]/g, "")
            }
            console.log("  ".repeat(indentLevel) + "๐Ÿ”„", log.trim())
            indentLevel++
            return
          }
          if (log.includes("Program return") || log.includes("Program consumed")) {
            indentLevel = Math.max(0, indentLevel - 1)
          }
    
          // Add indentation to all logs
          const indent = "  ".repeat(indentLevel)
    
          if (log.includes("Program log:")) {
            const logMessage = log.replace("Program log:", "").trim()
            if (log.includes("Program log:")) {
              console.log(indent + "๐Ÿ“", logMessage)
            } else if (log.includes("Program data:")) {
              console.log(indent + "๐Ÿ“Š", log.replace("Program data:", "").trim())
            }
          }
        })
        console.log("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n")
      } catch (error) {
        console.log("\nโŒ Transaction Failed")
        console.log("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”")
        console.error("Error:", error)
        console.log("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n")
      }
    }
    
    main()
    

    Note: The Program IDs and Access Controller Accounts are available on the Stream Addresses page.

  3. Add the snappy dependency to your project:

    yarn add snappy
    

    snappy is a compression library that is used to compress the report data.

  4. Execute the test script to interact with your program:

    ANCHOR_PROVIDER_URL="https://api.devnet.solana.com" ANCHOR_WALLET="~/.config/solana/id.json" ts-node tests/verify_test.ts
    

    Replace ~/.config/solana/id.json with the path to your Solana wallet (e.g., /Users/username/.config/solana/id.json).

  5. Verify the output logs to ensure the report data is processed correctly. Expect to see the decoded report fields logged to the console:

    ๐Ÿ“ Transaction Details
    โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
    ๐Ÿ”‘ Signer: 1BZZU8cJsrMSBaQQGUxTE4LQYX2SU2jjs97pkrz7rHD
    โœ… Transaction successful!
    ๐Ÿ”— Signature: 5wVCWAQUs7gZyCzbJDGs7UGHjbr3Z9YwyiV3PcpiJyKwZUVaUBf7BUAqX5CFGGfGWbBRu3PdfBRXCndeoeNPh4Ca
    
    ๐Ÿ“‹ Program Logs
    โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
    ๐Ÿ“ Instruction: Verify
    ๐Ÿ“ Instruction: Verify
    ๐Ÿ“ Report data found
    ๐Ÿ“ FeedId: 0x0003684ea93c43ed7bd00ab3bb189bb62f880436589f1ca58b599cd97d6007fb
    ๐Ÿ“ Valid from timestamp: 1733758884
    ๐Ÿ“ Observations Timestamp: 1733758884
    ๐Ÿ“ Native Fee: 84021511714900
    ๐Ÿ“ Link Fee: 12978571827423900
    ๐Ÿ“ Expires At: 1733845284
    ๐Ÿ“ Benchmark Price: 12302227135960220
    ๐Ÿ“ Bid: 12294760000000000
    ๐Ÿ“ Ask: 12304232715632312
    โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
    

Learn more

Program Derived Addresses (PDAs)

The verification process relies on two important PDAs that are handled automatically by the Chainlink Data Streams Solana SDK:

  • Verifier config account PDA:

    • Derived using the verifier program ID as a seed
    • Stores verification-specific configuration
    • Used to ensure consistent verification parameters across all reports
  • Report config account PDA:

    • Derived using the feed ID (first 32 bytes of the uncompressed signed report) as a seed
    • Contains feed-specific configuration and constraints
    • Ensures that each feed's verification follows its designated rules

The SDK's VerifierInstructions::verify helper method performs these steps:

  1. Extracts the necessary seeds
  2. Computes the PDAs using Pubkey::find_program_derived_address
  3. Includes these derived addresses in the instruction data

Best practices

This guide provides a basic example on how to verify reports. When you implement reports verification, consider the following best practices:

  • Implement robust error handling:
    • Handle verification failures and invalid reports comprehensively
    • Implement proper error reporting and logging for debugging
    • Add custom error types for different failure scenarios
  • Add appropriate validations:
    • Price threshold checks to prevent processing extreme values
    • Timestamp validations to ensure data freshness
    • Custom feed-specific validations based on your use case

What's next

Get the latest Chainlink content straight to your inbox.