Verify report data - Onchain integration (Stellar)
Guide Versions
This guide is available in multiple versions. Choose the one that matches your needs.
In this tutorial, you'll learn how to verify the integrity of Data Streams reports directly within your Soroban smart contract on the Stellar blockchain. The Chainlink Verifier contract validates signed reports using ECDSA multi-signature verification compatible with the Chainlink OCR2 protocol, confirming their authenticity as signed by the Decentralized Oracle Network (DON).
A single Verifier contract manages all feed configurations internally. No separate registry contract is needed.
Prerequisites
Before you begin, you should have:
- Familiarity with Rust programming
- Understanding of Stellar and Soroban smart contract concepts
- An allowlisted account in the Data Streams Access Controller. (Contact us to get started.)
Requirements
To complete this tutorial, you'll need:
-
Rust v1.84.0 or higher (excluding 1.91.0): Soroban contracts require Rust 1.84.0+ because the
wasm32v1-nonecompilation target is only available in recent toolchain versions. The Stellar CLI also blocks specific Rust versions that are known to produce broken WebAssembly output: 1.81, 1.82, 1.83, and 1.91.0 are all rejected at build time.Install Rust using rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shRun
rustc --versionto verify your version. If you're on a blocked version or need to update, run:rustup update stableNote: Rust 1.91.0 specifically contains a known WASM linker bug (patched in 1.91.1). If you see the error
use a rust version other than 1.81, 1.82, 1.83 or 1.91.0, runrustup update stableto install the latest patch release. -
WebAssembly target: Add the
wasm32v1-nonetarget required for building Soroban contracts:rustup target add wasm32v1-noneNote: The WebAssembly target is installed per-toolchain. If you update your Rust version, you'll need to reinstall this target for the new toolchain.
-
Stellar CLI: Used to build, deploy, and invoke contracts. Install the latest release:
# macOS / Linux (install script) curl -fsSL https://github.com/stellar/stellar-cli/raw/main/install.sh | sh # macOS / Linux (Homebrew) brew install stellar-cli # Cargo (all platforms) cargo install --locked stellar-cli@25.2.0Run
stellar --versionto verify your installation. See the Stellar CLI documentation for more details. -
Testnet account: You'll need a funded testnet account. Generate and fund one with the Stellar CLI:
stellar keys generate <YOUR_KEY_NAME> --network testnet --fund stellar keys public-key <YOUR_KEY_NAME>
Implementation
1. Create a new Soroban project
-
Use the Stellar CLI to initialize a new project. This creates a Rust workspace with the recommended Soroban contract structure:
stellar contract init consumer_example cd consumer_exampleThe generated project layout looks like this:
consumer_example/ ├── Cargo.toml ├── README.md └── contracts/ └── hello-world/ ├── Cargo.toml ├── Makefile └── src/ ├── lib.rs └── test.rs -
Rename the default contract directory to something meaningful:
mv contracts/hello-world contracts/consumer_example -
The root
Cargo.tomlsets up a Rust workspace and the sharedsoroban-sdkversion. It should look like this:[workspace] resolver = "2" members = [ "contracts/*", ] [workspace.dependencies] soroban-sdk = "25" [profile.release] opt-level = "z" overflow-checks = true debug = 0 strip = "symbols" debug-assertions = false panic = "abort" codegen-units = 1 lto = true # For more information about this profile see https://soroban.stellar.org/docs/basic-tutorials/logging#cargotoml-profile [profile.release-with-logs] inherits = "release" debug-assertions = trueThe release profile is critical — Soroban contracts have a maximum size of 64KB and without these settings most contracts will exceed that limit.
-
Open
contracts/consumer_example/Cargo.tomland update it to match the following. Lines highlighted in green are the changes from the generated default:contracts/consumer_example/Cargo.tomlPlaintext1 [package] 2 name = "consumer_example" 3 version = "0.1.0" 4 edition = "2021" 5 publish = false 6 7 [lib] 8 crate-type = ["cdylib"] 9 doctest = false 10 11 [dependencies] 12 soroban-sdk = { workspace = true } 13 14 [dev-dependencies] 15 soroban-sdk = { workspace = true, features = ["testutils"] } 16
- Line 2: Rename the package from
hello-worldtoconsumer_example. - Line 3: Update the version to
0.1.0. - Line 8: Remove
"lib"fromcrate-type, keeping only"cdylib". This ensures the output is a.wasmfile suitable for deployment. Including"lib"would produce an additional native library that is not needed.
2. Declare the Verifier interface
Create a new file at contracts/consumer_example/src/verifier_interface.rs:
touch contracts/consumer_example/src/verifier_interface.rs
This is the Soroban equivalent of a Solidity interface — declare only the function signatures you need. No WASM file is required. Open the file and add the following:
// verifier_interface.rs
use soroban_sdk::{contractclient, Address, Bytes, Env};
#[contractclient(name = "VerifierClient")]
pub trait VerifierInterface {
fn verify(env: Env, signed_report: Bytes, sender: Address) -> Bytes;
}
The #[contractclient] attribute generates a VerifierClient struct that handles cross-contract invocation to the deployed Verifier contract address at runtime.
3. Call the Verifier from your contract
In contracts/consumer_example/src/lib.rs, replace the default contents with the following. This imports the verifier_interface module you created in the previous step and calls the Verifier's verify function using the generated client.
// lib.rs
#![no_std]
mod verifier_interface;
use verifier_interface::VerifierClient;
use soroban_sdk::{contract, contractimpl, Address, Bytes, Env};
#[contract]
pub struct Consumer;
#[contractimpl]
impl Consumer {
pub fn consume_price_data(
env: Env,
verifier_address: Address,
signed_report: Bytes,
sender: Address,
) -> Bytes {
sender.require_auth();
let verifier = VerifierClient::new(&env, &verifier_address);
// Verifies all ECDSA signatures and returns the decoded report data.
// The config digest is extracted from the report internally.
let report_data = verifier.verify(&signed_report, &sender);
// Process report_data — it is EVM-encoded.
// The first 32 bytes are the feed_id; remaining bytes are feed-specific.
report_data
}
}
4. Understand report verification
On each call to verify, the Verifier contract performs the following steps:
- Parses the EVM-encoded signed report
- Extracts the
configDigestfromreportContext[0] - Looks up the registered configuration and asserts it is active
- Hashes the report data with
keccak256 - Recovers each signer address via ECDSA (
ecrecover) - Asserts that exactly
f+1valid, non-duplicate signatures from registered oracles are present - Returns the raw
report_databytes on success, or panics with aContractErroron failure
5. Process the report data
The report_data bytes returned by the Verifier are EVM-encoded. Decode them according to the specific feed schema.
- The first 32 bytes of
report_dataare always thefeed_id. - The remaining fields (timestamps, prices, etc.) are feed-specific.
The encoding format and schema details can be found in the Report Schemas documentation or the API specification for your feed.
Build and deploy
Run tests
cargo test
The included real_signature_tests exercise the full verification flow with 16 registered signers, f=5, and 6 real ECDSA signatures — no live network required.
Build your contract
Use the Stellar CLI build command, which automatically targets wasm32v1-none and the release profile:
stellar contract build
This is equivalent to:
cargo build --target wasm32v1-none --release
Output: target/wasm32v1-none/release/consumer_example.wasm
Tip: If you get an error like
can't find crate for 'core', you didn't install thewasm32v1-nonetarget. Runrustup target add wasm32v1-noneand try again.
Deploy to testnet
stellar contract deploy \
--wasm target/wasm32v1-none/release/consumer_example.wasm \
--source <YOUR_KEY_NAME> \
--network testnet
Replace <YOUR_KEY_NAME> with the name you chose when running stellar keys generate. Run stellar keys ls to list your available keys.
Expected output
After a successful deploy, the CLI uploads the WASM, submits two transactions (upload and deploy), prints explorer links, and ends with the new contract address. Transaction hashes, wasm hash, links, and the contract ID will be unique to your run; the flow below is representative:
ℹ️ Uploading contract WASM…
ℹ️ Simulating transaction…
ℹ️ Signing transaction: 5cb88fd2931e869605c9e96438b727a21a25c35d986702d7b7ca368cae5e6c64
🌎 Sending transaction…
✅ Transaction submitted successfully!
🔗 https://stellar.expert/explorer/testnet/tx/5cb88fd2931e869605c9e96438b727a21a25c35d986702d7b7ca368cae5e6c64
ℹ️ Deploying contract using wasm hash b7019d9a0659a34e0ceb21f969f81c4c540148aaf098ea14288e8b8ca0726257
ℹ️ Simulating transaction…
ℹ️ Signing transaction: 67e8a8d8f452f670a4c77d65d863116c7f725b4010a65d1d5f8176b498bd0290
🌎 Sending transaction…
✅ Transaction submitted successfully!
🔗 https://stellar.expert/explorer/testnet/tx/67e8a8d8f452f670a4c77d65d863116c7f725b4010a65d1d5f8176b498bd0290
🔗 https://lab.stellar.org/r/testnet/contract/CDHXWMICLVZ6JILOTVESOOERWDCYS3VZ63UKXMJEKAYJLMACOA7N5G7D
✅ Deployed!
CDHXWMICLVZ6JILOTVESOOERWDCYS3VZ63UKXMJEKAYJLMACOA7N5G7D
Use the final line (the contract address) as <YOUR_CONSUMER_CONTRACT_ID> in the invoke commands below.
Invoke your contract
Call consume_price_data on your deployed consumer contract. The --signed_report argument takes raw hex with no 0x prefix.
The Stellar Verifier validates signatures against DON configurations registered specifically for it. You must use a signed report that was produced for the Stellar testnet Verifier — reports from EVM or Solana flows will fail with error #9 (DigestNotSet) because their configDigest is not registered on the Stellar Verifier.
The following is an example signed report for the Stellar testnet Verifier. Use it for --signed_report to test your integration:
00090d9e8d96765a0c49e03a6ae05c82e8f8de70cf179baa632f18313e54bd6900000000000000000000000000000000000000000000000000000000055dec11000000000000000000000000000000000000000000000000000000030000000100000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000028001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000120000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba7820000000000000000000000000000000000000000000000000000000069cfb8ca0000000000000000000000000000000000000000000000000000000069cfb8ca00000000000000000000000000000000000000000000000000008df4dc2c7e950000000000000000000000000000000000000000000000000082e2f86a9a40fd0000000000000000000000000000000000000000000000000000000069f745ca00000000000000000000000000000000000000000000006f24528cda20d2bd7000000000000000000000000000000000000000000000006f2415d249c56f254000000000000000000000000000000000000000000000006f25889bbe62ce70000000000000000000000000000000000000000000000000000000000000000002e0b88dec92d81f05ff7d1fced36e40b9b1a0f83ef0397fcda201abdca73b920599cb9d047d4e32c385a384801c306874cce05ca3f0f4e2ade196ad136ff42a8900000000000000000000000000000000000000000000000000000000000000025f86ce4a0315adbe7c9c62127744431f49a539b717ba454638158b853e99a6493e930836862f9ea99c02f6faf30e5a96e4faf96c277d2ca1e102eb66b5d688e0
Simulate without submitting a transaction:
stellar contract invoke \
--id <YOUR_CONSUMER_CONTRACT_ID> \
--network testnet \
--source <YOUR_KEY_NAME> \
--send=no \
-- consume_price_data \
--verifier_address CA7GVHWH4GRHE6GI7MHEKQZAOYO4GE7KRGSU3EOS3HYJRVLX3XEA4ONQ \
--signed_report <hex_encoded_report> \
--sender <YOUR_KEY_NAME>
Submit the transaction:
stellar contract invoke \
--id <YOUR_CONSUMER_CONTRACT_ID> \
--network testnet \
--source <YOUR_KEY_NAME> \
-- consume_price_data \
--verifier_address CA7GVHWH4GRHE6GI7MHEKQZAOYO4GE7KRGSU3EOS3HYJRVLX3XEA4ONQ \
--signed_report <hex_encoded_report> \
--sender <YOUR_KEY_NAME>
On success, the CLI prints the hex-encoded report_data and the verified event containing the feed_id.
Example testnet report
The following is an example invoke command using the testnet report above. Feed ID: 000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
stellar contract invoke \
--id <YOUR_CONSUMER_CONTRACT_ID> \
--network testnet \
--source <YOUR_KEY_NAME> \
--send=no \
-- consume_price_data \
--verifier_address CA7GVHWH4GRHE6GI7MHEKQZAOYO4GE7KRGSU3EOS3HYJRVLX3XEA4ONQ \
--signed_report 00090d9e8d96765a0c49e03a6ae05c82e8f8de70cf179baa632f18313e54bd6900000000000000000000000000000000000000000000000000000000055dec11000000000000000000000000000000000000000000000000000000030000000100000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000028001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000120000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba7820000000000000000000000000000000000000000000000000000000069cfb8ca0000000000000000000000000000000000000000000000000000000069cfb8ca00000000000000000000000000000000000000000000000000008df4dc2c7e950000000000000000000000000000000000000000000000000082e2f86a9a40fd0000000000000000000000000000000000000000000000000000000069f745ca00000000000000000000000000000000000000000000006f24528cda20d2bd7000000000000000000000000000000000000000000000006f2415d249c56f254000000000000000000000000000000000000000000000006f25889bbe62ce70000000000000000000000000000000000000000000000000000000000000000002e0b88dec92d81f05ff7d1fced36e40b9b1a0f83ef0397fcda201abdca73b920599cb9d047d4e32c385a384801c306874cce05ca3f0f4e2ade196ad136ff42a8900000000000000000000000000000000000000000000000000000000000000025f86ce4a0315adbe7c9c62127744431f49a539b717ba454638158b853e99a6493e930836862f9ea99c02f6faf30e5a96e4faf96c277d2ca1e102eb66b5d688e0 \
--sender <YOUR_KEY_NAME>
Expected output:
📅 CA7GVHWH4GRHE6GI7MHEKQZAOYO4GE7KRGSU3EOS3HYJRVLX3XEA4ONQ - Success - Event: [{"symbol":"verified"}] = {"vec":[{"bytes":"000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782"},{"address":"<YOUR_SENDER_ADDRESS>"}]}
"000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782 ← feed_id
0000000000000000000000000000000000000000000000000000000069cfb8ca ← validFromTimestamp
0000000000000000000000000000000000000000000000000000000069cfb8ca ← observationsTimestamp
00000000000000000000000000000000000000000000000000008df4dc2c7e95 ← nativeFee
0000000000000000000000000000000000000000000000000082e2f86a9a40fd ← linkFee
0000000000000000000000000000000000000000000000000000000069f745ca ← expiresAt
00000000000000000000000000000000000000000000006f24528cda20d2bd70 ← benchmark price
00000000000000000000000000000000000000000000006f2415d249c56f2540 ← bid
00000000000000000000000000000000000000000000006f25889bbe62ce7000" ← ask
The first line shows the verified event emitted by the Verifier contract, confirming the report was accepted. The quoted hex string that follows is the report_data returned by your consume_price_data function — a single concatenated hex string containing all decoded feed fields.
nativeFee and linkFee are legacy onchain verification fee fields in the report schema. Data Streams uses subscription-based billing, so these fields are not used to charge per-verification fees.
You can view the submitted transaction on Stellar Expert as a reference example.
Network addresses
| Network | Verifier Proxy Address |
|---|
Reference
Key functions
| Function | Description |
|---|---|
verify(signed_report, sender) | Verifies a signed report and returns the decoded report_data bytes. Panics on failure. |
set_config(config_digest, signers, f) | Registers a new DON configuration. Owner only. |
update_config(config_digest, prev_signers, new_signers, f) | Replaces signers for an existing configuration. Owner only. |
activate_config(config_digest) | Activates a deactivated configuration. Owner only. |
deactivate_config(config_digest) | Deactivates a configuration. Owner only. |
transfer_ownership(proposed_owner) | Initiates a two-step ownership transfer. Owner only. |
accept_ownership() | Completes the ownership transfer. Proposed owner only. |
extend_contract_ttl() | Extends the contract's onchain TTL. Callable by anyone. |
is_initialized() | Returns true if the contract has been initialized. |
Configuration parameters
| Parameter | Description |
|---|---|
config_digest | 32-byte identifier for the DON configuration, derived from the feed. |
signers | List of 20-byte Ethereum-style oracle signer addresses. |
f | Fault tolerance threshold. Requires num_signers > 3 * f and f > 0. Reports must contain exactly f+1 signatures. |
Error reference
| Error | Code | Description |
|---|---|---|
NotInitialized | 2 | Contract has not been initialized. |
ZeroAddress | 3 | A signer address is the zero address. |
FaultToleranceMustBePositive | 4 | f must be greater than 0. |
ExcessSigners | 5 | More than 31 signers provided. |
InsufficientSigners | 6 | num_signers does not satisfy num_signers > 3 * f. |
NonUniqueSignatures | 7 | Duplicate signature detected. |
DigestEmpty | 8 | Config digest is all zeros. |
DigestNotSet | 9 | No configuration exists for this config digest. |
DigestInactive | 10 | Configuration has been deactivated. |
ConfigDigestAlreadySet | 11 | A configuration with this digest already exists. |
BadVerification | 12 | Recovered signer address not found in configuration. |
MismatchedSignatures | 13 | rs and ss arrays have different lengths. |
IncorrectSignatureCount | 14 | Signature count does not equal f+1. |
NoProposedOwner | 15 | No pending ownership transfer. |
InvalidReportFormat | 16 | Report bytes cannot be parsed. |
SignatureRecoveryFailed | 17 | ECDSA recovery failed for a signature. |
InvalidProposedOwner | 18 | Proposed owner is the same as current owner. |