# Onchain report verification (EVM chains)
Source: https://docs.chain.link/data-streams/reference/data-streams-api/onchain-verification

> For the complete documentation index, see [llms.txt](/llms.txt).

> **CAUTION: Onchain Data Verification**
>
> Onchain verification ensures the integrity of reports by confirming their authenticity as signed by the Decentralized
> Oracle Network (DON). It is the responsibility of the application contract(s) to determine the suitability of the
> report data for any further actions, such as trade execution.

> **NOTE: Get started**
>
> Data Streams is self-serve, no sales call required. [Sign up](https://app.chain.link) to get started, or follow the
> [sign-up guide](/data-streams/sign-up).

## About Verification

[When you fetch a report](/data-streams/tutorials/go-sdk-fetch) from Data Streams, you receive a [signed payload](/data-streams/tutorials/go-sdk-fetch#payload-for-onchain-verification). Onchain verification submits that payload to Chainlink's `VerifierProxy` contract, which checks the DON's signature and returns the decoded report data your contract can use. Your contract never talks to the Verifier contract directly: the proxy handles routing.

The `IVerifierProxy` interface exposes two verification functions:

| Function                      | Use case                                                |
| ----------------------------- | ------------------------------------------------------- |
| [`verify()`](#verify)         | Verify a single report payload in one transaction       |
| [`verifyBulk()`](#verifybulk) | Verify multiple report payloads in a single transaction |

**What Chainlink deploys:** The `VerifierProxy` contract. You only need its address, which is listed on the [Stream Addresses](/data-streams/crypto-streams) page.

**What you deploy:** Your own contract that calls `VerifierProxy.verify()` or `verifyBulk()`. The [contract example](#contract-example) below shows the full pattern and is a starting point — see the disclaimer before using it in production.

## IVerifierProxy interface

`IVerifierProxy` is the single onchain entry point for report verification. Your contract calls it directly — you never interact with the underlying Verifier contract. The proxy routes each call to the correct Verifier and returns the verified report data.

You will need the deployed `VerifierProxy` address for the network you are targeting. Find it on the [Stream Addresses](/data-streams/crypto-streams) page.

```solidity
interface IVerifierProxy {
  // Verify a single report. Returns the verified report body as ABI-encoded bytes.
  // Decode the return value into the appropriate report struct (v3, v8, etc.).
  function verify(
    bytes calldata payload,           // Full report payload from the Streams API
    bytes calldata parameterPayload   // pass empty fee metadata for Data Streams subscription billing
  ) external payable returns (bytes memory verifierResponse);

  // Verify multiple reports in one transaction. Accepts different feed IDs in the same call.
  // Returns verified report bytes in the same order as the input array.
  function verifyBulk(
    bytes[] calldata payloads,        // Array of report payloads — may span different feed IDs
    bytes calldata parameterPayload   // pass empty fee metadata for Data Streams subscription billing
  ) external payable returns (bytes[] memory verifiedReports);

  // Legacy FeeManager accessor. Data Streams verification does not require
  // per-verification fee quoting or approvals.
  function s_feeManager() external view returns (IVerifierFeeManager);
}
```

### `verify()`

Verifies a single report payload. Pass the raw payload bytes returned by the Streams API directly as `payload` — no transformation is needed.

| Parameter          | Type             | Description                                                                                                                               |
| ------------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `payload`          | `bytes calldata` | The full report payload returned by the Streams API (header + signed report body).                                                        |
| `parameterPayload` | `bytes calldata` | Fee metadata parameter retained for legacy fee-manager integrations. Pass empty bytes `""` for current Data Streams subscription billing. |

#### Returns

| Return             | Type           | Description                                                                                                                                                           |
| ------------------ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `verifierResponse` | `bytes memory` | ABI-encoded verified report body. Decode into the appropriate report struct according to the [report schema version](/data-streams/reference/report-schema-overview). |

#### Fee handling for `verify()`

Data Streams uses subscription-based billing. Your contract does not need to quote or approve a per-verification fee before calling `verify()`, and no supported EVM chain requires LINK funding for this step. Pass empty bytes `""` as `parameterPayload` because no fee token metadata is required.

### `verifyBulk()`

Verifies multiple report payloads in a single transaction. Reports may reference different feed IDs, enabling atomic multi-feed updates. Pass the raw payload bytes from the Streams API directly — no transformation is needed.

| Parameter          | Type               | Description                                                                                                                               |
| ------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `payloads`         | `bytes[] calldata` | Array of full report payloads from the Streams API. Each entry may correspond to a different feed ID. Order is preserved in the output.   |
| `parameterPayload` | `bytes calldata`   | Fee metadata parameter retained for legacy fee-manager integrations. Pass empty bytes `""` for current Data Streams subscription billing. |

#### Returns

| Return            | Type             | Description                                                                                                                        |
| ----------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `verifiedReports` | `bytes[] memory` | Array of ABI-encoded verified report bodies in the same order as `payloads`. Decode each entry into the appropriate report struct. |

#### Fee handling for `verifyBulk()`

Data Streams uses subscription-based billing. Your contract does not need to quote, sum, or approve per-report verification fees before calling `verifyBulk()`. Pass empty bytes `""` as `parameterPayload` because no fee token metadata is required.

## When to use `verifyBulk()`

Use `verifyBulk()` when your protocol needs price data from multiple feeds in the same block. Common use cases include:

- Calculating a portfolio's value across multiple asset prices atomically.
- Executing a trade that depends on both a base and a quote asset price (e.g. ETH/USD and BTC/USD).
- Updating multiple onchain price references in a single transaction to reduce state inconsistency.

`verifyBulk()` accepts multiple feed IDs in the same call — there is no requirement that all payloads reference the same stream.

## Gas considerations

`verifyBulk()` reduces overhead compared to issuing N separate `verify()` calls, each of which incurs an additional base transaction cost (21,000 gas). The per-report cryptographic verification cost is the same regardless of which function you use.

- **What you save**: one base transaction cost (21,000 gas) per additional report beyond the first.
- **What you do not save**: the cryptographic verification cost per report.
- **Fee cost**: Data Streams does not charge a per-report onchain verification fee.

## Contract example

The contract below is an **example you deploy yourself**. It demonstrates the full verification pattern for both `verifyReport()` (single report) and `verifyBulkReports()` (multiple reports). Use it as a reference when writing your own contract.

> **CAUTION: Disclaimer**
>
> This guide represents an example of using a Chainlink product or service and is provided to help you understand how to
> interact with Chainlink's systems and services so that you can integrate them into your own. This template is provided
> "AS IS" and "AS AVAILABLE" without warranties of any kind, has not been audited, and may be missing key checks or
> error handling to make the usage of the product more clear. Do not use the code in this example in a production
> environment without completing your own audits and application of best practices. Neither Chainlink Labs, the
> Chainlink Foundation, nor Chainlink node operators are responsible for unintended outputs that are generated due to
> errors in code.

```sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/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.
 *
 *  This contract verifies Chainlink Data Streams reports onchain. It exposes
 *  two verification functions:
 *
 *  - `verifyReport()`      – verifies a single report payload.
 *  - `verifyBulkReports()` – verifies multiple payloads (across any feed IDs)
 *                             in a single transaction using `verifyBulk()`.
 *
 *  Data Streams uses subscription-based billing. Verification calls do not
 *  require this contract to hold LINK or approve a FeeManager.
 */

// ────────────────────────────────────────────────────────────────────────────
//  Interface
// ────────────────────────────────────────────────────────────────────────────

interface IVerifierProxy {
  /**
   * @notice Route a report to the correct verifier.
   * @param payload           Full report payload (header + signed report).
   * @param parameterPayload  Empty fee metadata for Data Streams subscription billing.
   */
  function verify(
    bytes calldata payload,
    bytes calldata parameterPayload
  ) external payable returns (bytes memory verifierResponse);

  /**
   * @notice Route multiple reports to the correct verifier in a single call.
   * @param payloads          Array of full report payloads. Each entry may reference
   *                          a different feed ID. Order is preserved in the output.
   * @param parameterPayload  Empty fee metadata for Data Streams subscription billing.
   * @return verifiedReports  Verified report bytes in the same order as the input.
   */
  function verifyBulk(
    bytes[] calldata payloads,
    bytes calldata parameterPayload
  ) external payable returns (bytes[] memory verifiedReports);
}

// ────────────────────────────────────────────────────────────────────────────
//  Contract
// ────────────────────────────────────────────────────────────────────────────

/**
 * @dev This contract implements functionality to verify Data Streams reports from
 * the Data Streams API.
 */
contract ClientReportsVerifier {
  // ----------------- Errors -----------------
  error NothingToWithdraw();
  error NotOwner(address caller);
  error InvalidReportVersion(uint16 version);
  error EmptyReportsArray();

  // ----------------- Report schemas -----------------
  // More info: https://docs.chain.link/data-streams/reference/report-schema-v3
  /**
   * @dev Data Streams report schema v3 (crypto streams).
   *      Prices, bids and asks use 8 or 18 decimals depending on the stream.
   */
  struct ReportV3 {
    bytes32 feedId;
    uint32 validFromTimestamp;
    uint32 observationsTimestamp;
    uint192 nativeFee;
    uint192 linkFee;
    uint32 expiresAt;
    int192 price;
    int192 bid;
    int192 ask;
  }

  /**
   * @dev Data Streams report schema v8 (RWA streams).
   */
  struct ReportV8 {
    bytes32 feedId;
    uint32 validFromTimestamp;
    uint32 observationsTimestamp;
    uint192 nativeFee;
    uint192 linkFee;
    uint32 expiresAt;
    uint64 lastUpdateTimestamp;
    int192 midPrice;
    uint32 marketStatus;
  }

  // ----------------- Storage -----------------
  IVerifierProxy public immutable i_verifierProxy;
  address private immutable i_owner;

  int192 public lastDecodedPrice;
  int192[] public lastDecodedPrices;

  // ----------------- Events -----------------
  event DecodedPrice(int192 price);

  // ----------------- Constructor / modifier -----------------
  /**
   * @param _verifierProxy Address of the VerifierProxy on the target network.
   *        Addresses: https://docs.chain.link/data-streams/crypto-streams
   */
  constructor(
    address _verifierProxy
  ) {
    i_owner = msg.sender;
    i_verifierProxy = IVerifierProxy(_verifierProxy);
  }

  modifier onlyOwner() {
    if (msg.sender != i_owner) revert NotOwner(msg.sender);
    _;
  }

  // ----------------- Public API -----------------

  /**
   * @notice Verify a Data Streams report (schema v3 or v8).
   *
   * @dev Steps:
   *  1. Decode the unverified report to get `reportData`.
   *  2. Read the first two bytes → schema version (`0x0003` or `0x0008`).
   *     - Revert if the version is unsupported.
   *  3. Call `VerifierProxy.verify()` with empty fee metadata. Data Streams
   *     uses subscription billing, so no fee token address is required.
   *  4. Decode the verified report into the correct struct and emit the price.
   *
   *  @param unverifiedReport Full payload returned by Streams Direct.
   *  @custom:reverts InvalidReportVersion when schema ≠ v3/v8.
   */
  function verifyReport(
    bytes memory unverifiedReport
  ) external {
    // ─── 1. & 2. Extract reportData and schema version ──
    (, bytes memory reportData) = abi.decode(unverifiedReport, (bytes32[3], bytes));

    uint16 reportVersion = (uint16(uint8(reportData[0])) << 8) | uint16(uint8(reportData[1]));
    if (reportVersion != 3 && reportVersion != 8) {
      revert InvalidReportVersion(reportVersion);
    }

    // ─── 3. Verify through the proxy ──
    bytes memory verified = i_verifierProxy.verify(unverifiedReport, bytes(""));

    // ─── 4. Decode & store price ──
    if (reportVersion == 3) {
      int192 price = abi.decode(verified, (ReportV3)).price;
      lastDecodedPrice = price;
      emit DecodedPrice(price);
    } else {
      int192 price = abi.decode(verified, (ReportV8)).midPrice;
      lastDecodedPrice = price;
      emit DecodedPrice(price);
    }
  }

  /**
   * @notice Verify multiple Data Streams reports (schema v3 or v8) in one transaction.
   *
   * @dev Steps:
   *  1. Decode each payload to extract `reportData` and the schema version.
   *     - Revert if any version is unsupported.
   *  2. Call `VerifierProxy.verifyBulk()` once with all payloads and an empty
   *     parameter payload.
   *  3. Decode each verified report, store prices in `lastDecodedPrices`,
   *     and emit a `DecodedPrice` event per report.
   *
   *  @param unverifiedReports Array of full payloads from Streams Direct.
   *         Each payload may reference a different feed ID.
   *  @custom:reverts EmptyReportsArray    when called with an empty array.
   *  @custom:reverts InvalidReportVersion when any schema version ≠ v3/v8.
   */
  function verifyBulkReports(
    bytes[] memory unverifiedReports
  ) external {
    if (unverifiedReports.length == 0) revert EmptyReportsArray();

    // ─── 1. Decode all payloads upfront ──
    bytes[] memory reportDataArray = new bytes[](unverifiedReports.length);
    uint16[] memory reportVersions = new uint16[](unverifiedReports.length);

    for (uint256 i = 0; i < unverifiedReports.length; i++) {
      (, bytes memory reportData) = abi.decode(unverifiedReports[i], (bytes32[3], bytes));

      uint16 reportVersion = (uint16(uint8(reportData[0])) << 8) | uint16(uint8(reportData[1]));
      if (reportVersion != 3 && reportVersion != 8) {
        revert InvalidReportVersion(reportVersion);
      }

      reportDataArray[i] = reportData;
      reportVersions[i] = reportVersion;
    }

    // ─── 2. Verify all reports in one proxy call ──
    bytes[] memory verifiedReports = i_verifierProxy.verifyBulk(unverifiedReports, bytes(""));

    // ─── 3. Decode verified reports, store prices, emit events ──
    int192[] memory prices = new int192[](verifiedReports.length);

    for (uint256 i = 0; i < verifiedReports.length; i++) {
      int192 price;
      if (reportVersions[i] == 3) {
        price = abi.decode(verifiedReports[i], (ReportV3)).price;
      } else {
        price = abi.decode(verifiedReports[i], (ReportV8)).midPrice;
      }
      prices[i] = price;
      emit DecodedPrice(price);
    }

    lastDecodedPrices = prices;
  }

  /**
   * @notice Withdraw all balance of an ERC-20 token held by this contract.
   * @param _beneficiary Address that receives the tokens.
   * @param _token       ERC-20 token address.
   */
  function withdrawToken(
    address _beneficiary,
    address _token
  ) external onlyOwner {
    uint256 amount = IERC20(_token).balanceOf(address(this));
    if (amount == 0) revert NothingToWithdraw();
    IERC20(_token).safeTransfer(_beneficiary, amount);
  }
}
```

### `verifyReport()` — single report

- **Input**: one `bytes` payload from the Streams API — pass it directly.
- **Fee**: passes empty bytes for `parameterPayload`; no per-verification LINK fee is required.
- **Output**: stores the decoded price in `lastDecodedPrice` and emits `DecodedPrice(int192 price)`.

See the [Verify report data onchain (EVM)](/data-streams/tutorials/evm-onchain-report-verification) tutorial for a step-by-step walkthrough using this function.

### `verifyBulkReports()` — multiple reports

- **Input**: `bytes[]` array of payloads from the Streams API — each may reference a different feed ID.
- **Fee**: passes empty bytes for `parameterPayload`; no per-verification LINK fee is required.
- **Output**: stores the decoded prices in `lastDecodedPrices` (an `int192[]` array in the same order as the inputs) and emits `DecodedPrice(int192 price)` once per report.