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:
-
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 tutorial.
-
Offchain integration: Verify reports client-side using an SDK. Learn more about this method in the offchain verification tutorial.
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 tutorial, 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:
- Familiarity with Rust programming
- Understanding of Solana concepts:
- Knowledge of the Anchor framework
- An allowlisted account in the Data Streams Access Controller. (contact us to get started).
Requirements
To complete this tutorial, you'll need:
-
Rust and Cargo: Install Rust 1.79.0 or later using rustup. Run
rustc --versionto verify your installation. -
Solana CLI tools: Install Solana CLI 2.0 or later following the official guide. Run
solana --versionto verify your installation. -
Anchor Framework: Install Anchor 0.31.0 or later following the official installation guide. Run
anchor --versionto verify your installation.Important: Using mismatched Anchor versions between your program and client can cause type incompatibilities and runtime errors.
-
Node.js and npm: Install Node.js 20 or later. Verify your installation with
node --version. -
ts-node: Install globally using npm:
npm install -g ts-node. Verify your installation withts-node --version. -
Devnet SOL: You'll need devnet SOL for deployment and testing. Use the Solana CLI or the Solana Faucet to get devnet SOL.
Note: While this tutorial uses the Anchor framework for project structure, you can integrate the verification using any Rust-based Solana program framework. The verifier SDK is written in Rust, but you can integrate it into your preferred Rust program structure.
Implementation tutorial
1. Create a new Anchor project
-
Open your terminal and run the following command to create a new Anchor project:
anchor init example_verifyThis command creates a new directory named
example_verifywith the basic structure of an Anchor project. -
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:
[features]
seeds = false
skip-lint = false
[programs.devnet]
# Replace with your program ID
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]
anchor-lang = "0.31.0"
chainlink_solana_data_streams = { git = "https://github.com/smartcontractkit/chainlink-data-streams-solana" }
chainlink-data-streams-report = "1.0.3"
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::{
program::{get_return_data, invoke},
pubkey::Pubkey,
instruction::Instruction,
};
// NOTE: Adjust for your report version
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");
// NOTE: Adjust for your report version (V3, V4, V8, etc.)
let report = ReportDataV3::decode(&return_data)
.map_err(|_| error!(CustomError::InvalidReportData))?;
// Log report fields
// NOTE: Adjust for your report and desired output
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.
This example uses the V3 schema for crypto streams to decode the report. If you verify reports for RWA streams, import and use the V8 schema from the report crate instead.
5. Deploy your program
-
Build your program:
anchor build -
Deploy your program to devnet:
anchor deployExpect 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: 8XcUbDgY2UaUYNHkirKsWqXJtzPXezBSyj5Yh87dXums Signature: 3ky6VkpebDGq7x1n8JB32daybmjvbRBsD4yR2uCCussSWhokaEESTXuSa5s8NMvKTz2NZjoq9aoQ9pvuw9bYoibt 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.
-
In the
testsdirectory, create a new fileverify_test.tsto interact with your deployed program. -
Populate your
verify_tests.tsfile with the example client script below.- Replace
<YOUR_PROGRAM_ID>with your program ID. - This example provides a report payload. To use your own report payload, update the
hexStringvariable.
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 your program using the IDL and your program ID const program = new Program<ExampleVerify>(require("../target/idl/example_verify.json"), provider) // 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.
- Replace
-
Add the
snappydependency to your project:yarn add snappyAlso ensure you have the required TypeScript dependencies:
yarn add @solana/web3.js yarn add -D ts-node typescript @types/nodeNote:
snappyis a compression library used to compress the report data before sending it to the verifier. -
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.tsReplace
~/.config/solana/id.jsonwith the path to your Solana wallet (e.g.,/Users/username/.config/solana/id.json). -
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: 2CTZ7kgAxTogvMgb7QFDJUAq9xFBUVTEvyjf7UuhoVrHDhYKtHpQmd8hEy9XvLhfgWMdVTpCRvdf18r1ixgtncUc ๐ 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:
- Extracts the necessary seeds
- Computes the PDAs using
Pubkey::find_program_derived_address - Includes these derived addresses in the instruction data
Best practices
This tutorial 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
Adapting code for different report schema versions
When working with different versions of Data Stream reports, you'll need to adapt your code to handle the specific report schema version they use:
-
Import the correct schema version module. Examples:
-
For v3 schema (as used in this example):
use chainlink_data_streams_report::report::v3::ReportDataV3; -
For v8 schema:
use chainlink_data_streams_report::report::v8::ReportDataV8;
-
-
Update the decode function to use the correct schema version. Examples:
-
For v3 schema (as used in this example):
let report = ReportDataV3::decode(&return_data)?; -
For v8 schema:
let report = ReportDataV8::decode(&return_data)?;
-
-
Adjust report field access and logic for the schema version.
- The fields you access and log (e.g.,
feed_id,benchmark_price,bid,ask, etc.) must match the schema version's structure and available fields.
- The fields you access and log (e.g.,
Refer to the Report Schemas documentation for details on each version's fields and usage.