Verify report data onchain

Guide Versions

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

In this guide, you'll learn how to verify onchain the integrity of reports by confirming their authenticity as signed by the Decentralized Oracle Network (DON). You'll use a verifier contract to verify the data onchain and pay the verification fee in LINK tokens.

Before you begin

Make sure you understand how to use the Streams Direct implementation of Chainlink Data Streams to fetch reports via the REST API or WebSocket connection. Refer to the following guides for more information:

Requirements

Tutorial

Deploy the verifier contract

Deploy a ClientReportsVerifier contract on Arbitrum Sepolia. This contract is enabled to verify reports and pay the verification fee in LINK tokens.

  1. Open the ClientReportsVerifier.sol contract in Remix.

  2. Select the ClientReportsVerifier.sol contract in the Solidity Compiler tab.

    Chainlink Data Streams - Verify Report Data Onchain - Solidity Compiler
  3. Compile the contract.

  4. Open MetaMask and set the network to Arbitrum Sepolia. If you need to add Arbitrum Sepolia to your wallet, you can find the chain ID and the LINK token contract address on the LINK Token Contracts page.

  5. On the Deploy & Run Transactions tab in Remix, select Injected Provider - MetaMask in the Environment list. Remix will use the MetaMask wallet to communicate with Arbitrum Sepolia.

    Chainlink Data Streams - Verify Report Data Onchain - Injected Provider MetaMask
  6. In the Contract section, select the ClientReportsVerifier contract and fill in the Arbitrum Sepolia verifier proxy address: 0x2ff010DEbC1297f19579B4246cad07bd24F2488A. You can find the verifier proxy addresses on the Stream Addresses page.

    Chainlink Data Streams Remix Deploy ClientReportsVerifier Contract
  7. Click the Deploy button to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to ensure you deploy the contract to Arbitrum Sepolia.

  8. After you confirm the transaction, the contract address appears under the Deployed Contracts list in Remix. Save this contract address for the next step.

    Chainlink Data Streams Remix Deployed ClientReportsVerifier Contract

Fund the verifier contract

In this example, the verifier contract pays for onchain verification of reports in LINK tokens.

Open MetaMask and send 1 testnet LINK on Arbitrum Sepolia to the verifier contract address you saved earlier.

Verify a report onchain

  1. In Remix, on the Deploy & Run Transactions tab, expand your verifier contract under the Deployed Contracts section.

  2. Fill in the verifyReport function input parameter with the report payload you want to verify. You can use the following full report payload obtained in the Fetch and decode report via a REST API guide as an example:

    0x000660403d36be006d0c15d9b306f93c8660c5cfeab7db8e28c78ba316d395970000000000000000000000000000000000000000000000000000000032c3780a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000280010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001200003c8e550d2fc5304993010112de9b69798297e4cc11990ee6250e464daf760000000000000000000000000000000000000000000000000000000006706e595000000000000000000000000000000000000000000000000000000006706e595000000000000000000000000000000000000000000000000000025bd3eb74c080000000000000000000000000000000000000000000000000021c6a95c654c7400000000000000000000000000000000000000000000000000000000670837150000000000000000000000000000000000000000000000079a2ab4077fc8fc6000000000000000000000000000000000000000000000000799fcb42536dfd8300000000000000000000000000000000000000000000000079a59496c3f29a0000000000000000000000000000000000000000000000000000000000000000002bd4acd37ce3cd5799de05d156ab328a5effd94468ebbaf2ff18d13d9631259cbe66cca01af6a8bb36e79d2d731a44e16791ee31e46ce27ed6530f1590cd7734c0000000000000000000000000000000000000000000000000000000000000002391562f1f2e4986bdb978fbf5ee27f7012992a79301af42d3473761ef2ede6271a61fbf4b32ac5be68a598bcfa523e035b624dab3b3d9a46276834f824ee592a
    
    Chainlink Data Streams Remix Deployed ClientReportsVerifier Contract
  3. Click the verifyReport button to call the function. MetaMask prompts you to accept the transaction.

  4. Click the last_decoded_price getter function to view the decoded price from the verified report. The answer on the ETH/USD stream uses 18 decimal places, so an answer of 3257579704051546000000 indicates an ETH/USD price of 3,257.579704051546. Each stream uses a different number of decimal places for answers. See the Stream Addresses page for more information.

    Chainlink Data Streams - Price from Verified Report

Examine the code

The example code you deployed has all the interfaces and functions required to verify Data Streams reports onchain.

// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import {Common} from "@chainlink/contracts/src/v0.8/llo-feeds/libraries/Common.sol";
import {IRewardManager} from "@chainlink/contracts/src/v0.8/llo-feeds/interfaces/IRewardManager.sol";
import {IVerifierFeeManager} from "@chainlink/contracts/src/v0.8/llo-feeds/interfaces/IVerifierFeeManager.sol";
import {IERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";

using SafeERC20 for IERC20;

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE FOR DEMONSTRATION PURPOSES.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */

// Custom interfaces for IVerifierProxy and IFeeManager
interface IVerifierProxy {
    /**
     * @notice Verifies that the data encoded has been signed correctly by routing to the correct verifier, and bills the user if applicable.
     * @param payload The encoded data to be verified, including the signed report.
     * @param parameterPayload Fee metadata for billing. In the current implementation, this consists of the abi-encoded address of the ERC-20 token used for fees.
     * @return verifierResponse The encoded report from the verifier.
     */
    function verify(
        bytes calldata payload,
        bytes calldata parameterPayload
    ) external payable returns (bytes memory verifierResponse);

    /**
     * @notice Verifies multiple reports in bulk, ensuring that each is signed correctly, routes them to the appropriate verifier, and handles billing for the verification process.
     * @param payloads An array of encoded data to be verified, where each entry includes the signed report.
     * @param parameterPayload Fee metadata for billing. In the current implementation, this consists of the abi-encoded address of the ERC-20 token used for fees.
     * @return verifiedReports An array of encoded reports returned from the verifier.
     */
    function verifyBulk(
        bytes[] calldata payloads,
        bytes calldata parameterPayload
    ) external payable returns (bytes[] memory verifiedReports);

    function s_feeManager() external view returns (IVerifierFeeManager);
}

interface IFeeManager {
    /**
     * @notice Calculates the fee and reward associated with verifying a report, including discounts for subscribers.
     * This function assesses the fee and reward for report verification, applying a discount for recognized subscriber addresses.
     * @param subscriber The address attempting to verify the report. A discount is applied if this address is recognized as a subscriber.
     * @param unverifiedReport The report data awaiting verification. The content of this report is used to determine the base fee and reward, before considering subscriber discounts.
     * @param quoteAddress The payment token address used for quoting fees and rewards.
     * @return fee The fee assessed for verifying the report, with subscriber discounts applied where applicable.
     * @return reward The reward allocated to the caller for successfully verifying the report.
     * @return totalDiscount The total discount amount deducted from the fee for subscribers.
     */
    function getFeeAndReward(
        address subscriber,
        bytes memory unverifiedReport,
        address quoteAddress
    ) external returns (Common.Asset memory, Common.Asset memory, uint256);

    function i_linkAddress() external view returns (address);

    function i_nativeAddress() external view returns (address);

    function i_rewardManager() external view returns (address);
}

/**
 * @dev This contract implements functionality to verify Data Streams reports from
 * the Streams Direct API or WebSocket connection, with payment in LINK tokens.
 */
contract ClientReportsVerifier {
    error NothingToWithdraw(); // Thrown when a withdrawal attempt is made but the contract holds no tokens of the specified type.
    error NotOwner(address caller); // Thrown when a caller tries to execute a function that is restricted to the contract's owner.
    error InvalidReportVersion(uint16 version); // Thrown when an unsupported report version is provided to verifyReport.

    /**
     * @dev Represents a data report from a Data Streams stream for v3 schema (crypto streams).
     * The `price`, `bid`, and `ask` values are carried to either 8 or 18 decimal places, depending on the stream.
     * For more information, see https://docs.chain.link/data-streams/crypto-streams and https://docs.chain.link/data-streams/reference/report-schema
     */
    struct ReportV3 {
        bytes32 feedId; // The stream ID the report has data for.
        uint32 validFromTimestamp; // Earliest timestamp for which price is applicable.
        uint32 observationsTimestamp; // Latest timestamp for which price is applicable.
        uint192 nativeFee; // Base cost to validate a transaction using the report, denominated in the chain’s native token (e.g., WETH/ETH).
        uint192 linkFee; // Base cost to validate a transaction using the report, denominated in LINK.
        uint32 expiresAt; // Latest timestamp where the report can be verified onchain.
        int192 price; // DON consensus median price (8 or 18 decimals).
        int192 bid; // Simulated price impact of a buy order up to the X% depth of liquidity utilisation (8 or 18 decimals).
        int192 ask; // Simulated price impact of a sell order up to the X% depth of liquidity utilisation (8 or 18 decimals).
    }

    /**
     * @dev Represents a data report from a Data Streams stream for v4 schema (RWA stream).
     * The `price` value is carried to either 8 or 18 decimal places, depending on the stream.
     * The `marketStatus` indicates whether the market is currently open. Possible values: `0` (`Unknown`), `1` (`Closed`), `2` (`Open`).
     * For more information, see https://docs.chain.link/data-streams/rwa-streams and https://docs.chain.link/data-streams/reference/report-schema-v4
     */
    struct ReportV4 {
        bytes32 feedId; // The stream ID the report has data for.
        uint32 validFromTimestamp; // Earliest timestamp for which price is applicable.
        uint32 observationsTimestamp; // Latest timestamp for which price is applicable.
        uint192 nativeFee; // Base cost to validate a transaction using the report, denominated in the chain’s native token (e.g., WETH/ETH).
        uint192 linkFee; // Base cost to validate a transaction using the report, denominated in LINK.
        uint32 expiresAt; // Latest timestamp where the report can be verified onchain.
        int192 price; // DON consensus median benchmark price (8 or 18 decimals).
        uint32 marketStatus; // The DON's consensus on whether the market is currently open.
    }

    IVerifierProxy public s_verifierProxy; // The VerifierProxy contract used for report verification.

    address private s_owner; // The owner of the contract.
    int192 public lastDecodedPrice; // Stores the last decoded price from a verified report.

    event DecodedPrice(int192 price); // Event emitted when a report is successfully verified and decoded.

    /**
     * @param _verifierProxy The address of the VerifierProxy contract.
     * You can find these addresses on https://docs.chain.link/data-streams/crypto-streams.
     */
    constructor(address _verifierProxy) {
        s_owner = msg.sender;
        s_verifierProxy = IVerifierProxy(_verifierProxy);
    }

    /// @notice Checks if the caller is the owner of the contract.
    modifier onlyOwner() {
        if (msg.sender != s_owner) revert NotOwner(msg.sender);
        _;
    }

    /**
     * @notice Verifies an unverified data report and processes its contents, supporting both v3 and v4 report schemas.
     * @dev Performs the following steps:
     * - Decodes the unverified report to extract the report data.
     * - Extracts the report version by reading the first two bytes of the report data.
     *   - The first two bytes correspond to the schema version encoded in the stream ID.
     *   - Schema version `0x0003` corresponds to report version 3 (for Crypto assets).
     *   - Schema version `0x0004` corresponds to report version 4 (for Real World Assets).
     * - Validates that the report version is either 3 or 4; reverts with `InvalidReportVersion` otherwise.
     * - Retrieves the fee manager and reward manager contracts.
     * - Calculates the fee required for report verification using the fee manager.
     * - Approves the reward manager to spend the calculated fee amount.
     * - Verifies the report via the VerifierProxy contract.
     * - Decodes the verified report data into the appropriate report struct (`ReportV3` or `ReportV4`) based on the report version.
     * - Emits a `DecodedPrice` event with the price extracted from the verified report.
     * - Updates the `lastDecodedPrice` state variable with the price from the verified report.
     * @param unverifiedReport The encoded report data to be verified, including the signed report and metadata.
     * @custom:reverts InvalidReportVersion(uint8 version) Thrown when an unsupported report version is provided.
     */
    function verifyReport(bytes memory unverifiedReport) external {
        // Retrieve fee manager and reward manager
        IFeeManager feeManager = IFeeManager(
            address(s_verifierProxy.s_feeManager())
        );

        IRewardManager rewardManager = IRewardManager(
            address(feeManager.i_rewardManager())
        );

        // Decode unverified report to extract report data
        (, bytes memory reportData) = abi.decode(
            unverifiedReport,
            (bytes32[3], bytes)
        );

        // Extract report version from reportData
        uint16 reportVersion = (uint16(uint8(reportData[0])) << 8) |
            uint16(uint8(reportData[1]));

        // Validate report version
        if (reportVersion != 3 && reportVersion != 4) {
            revert InvalidReportVersion(uint8(reportVersion));
        }

        // Set the fee token address (LINK in this case)
        address feeTokenAddress = feeManager.i_linkAddress();

        // Calculate the fee required for report verification
        (Common.Asset memory fee, , ) = feeManager.getFeeAndReward(
            address(this),
            reportData,
            feeTokenAddress
        );

        // Approve rewardManager to spend this contract's balance in fees
        IERC20(feeTokenAddress).approve(address(rewardManager), fee.amount);

        // Verify the report through the VerifierProxy
        bytes memory verifiedReportData = s_verifierProxy.verify(
            unverifiedReport,
            abi.encode(feeTokenAddress)
        );

        // Decode verified report data into the appropriate Report struct based on reportVersion
        if (reportVersion == 3) {
            // v3 report schema
            ReportV3 memory verifiedReport = abi.decode(
                verifiedReportData,
                (ReportV3)
            );

            // Log price from the verified report
            emit DecodedPrice(verifiedReport.price);

            // Store the price from the report
            lastDecodedPrice = verifiedReport.price;
        } else if (reportVersion == 4) {
            // v4 report schema
            ReportV4 memory verifiedReport = abi.decode(
                verifiedReportData,
                (ReportV4)
            );

            // Log price from the verified report
            emit DecodedPrice(verifiedReport.price);

            // Store the price from the report
            lastDecodedPrice = verifiedReport.price;
        }
    }

    /**
     * @notice Withdraws all tokens of a specific ERC20 token type to a beneficiary address.
     * @dev Utilizes SafeERC20's safeTransfer for secure token transfer. Reverts if the contract's balance of the specified token is zero.
     * @param _beneficiary Address to which the tokens will be sent. Must not be the zero address.
     * @param _token Address of the ERC20 token to be withdrawn. Must be a valid ERC20 token contract.
     */
    function withdrawToken(
        address _beneficiary,
        address _token
    ) public onlyOwner {
        // Retrieve the balance of this contract for the specified token
        uint256 amount = IERC20(_token).balanceOf(address(this));

        // Revert if there is nothing to withdraw
        if (amount == 0) revert NothingToWithdraw();

        // Transfer the tokens to the beneficiary
        IERC20(_token).safeTransfer(_beneficiary, amount);
    }
}

Initializing the contract

When deploying the contract, you define the verifier proxy address for the stream you want to read from. You can find this address on the Stream Addresses page. The verifier proxy address provides functions that are required for this example:

  • The s_feeManager function to estimate the verification fees.
  • The verify function to verify the report onchain.

Verifying a report

The verifyReport function is the core function that handles onchain report verification. Here's how it works:

  • Report data extraction:

    • The function decodes the unverifiedReport to extract the report data.
    • It then extracts the report version by reading the first two bytes of the report data, which correspond to the schema version encoded in the stream ID:
    • If the report version is unsupported, the function reverts with an InvalidReportVersion error.
  • Fee calculation:

    • The function interacts with the FeeManager contract (accessed via the verifier proxy) to calculate the fees required for verification.
    • It uses the getFeeAndReward function to obtain the fee amount in LINK tokens.
  • Token approval: It approves the RewardManager contract to spend the calculated amount of LINK tokens from the contract's balance.

  • Report verification:

    • The verify function of the verifier proxy is called to perform the actual verification.
    • It passes the unverifiedReport and the encoded fee token address as parameters.
  • Data decoding:

    • Depending on the report version, the function decodes the verified report data into the appropriate struct (ReportV3 or ReportV4).
    • It emits a DecodedPrice event with the price extracted from the verified report.
    • The lastDecodedPrice state variable is updated with the new price.

What's next

Get the latest Chainlink content straight to your inbox.