Blockchain Oracle
Oracle API allows you to perform SQL queries over massive volumes of data in the ODF network directly from smart contracts.
It’s the first of a kind optimistic oracle that enables reproducibility and verifiability of requests without an expensive consensus protocol (more on this below).
Example
Example of the Solidity contract using ODF oracle:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;
import { OdfRequest, OdfResponse, IOdfClient, CborReader } from "@opendatafabric/contracts/Odf.sol";
// This sample contract makes an ODF oracle query to calculate
// a Canadian province with the most recorded COVID-19 cases
contract TestConsumer {
using OdfRequest for OdfRequest.Req;
using OdfResponse for OdfResponse.Res;
using CborReader for CborReader.CBOR;
IOdfClient private oracle;
string public province;
int public totalCases;
// Initialize contract with the oracle address
constructor(address oracleAddr) {
oracle = IOdfClient(oracleAddr);
}
function initiateQuery() public {
OdfRequest.Req memory req = OdfRequest.init();
// Specify ID of the dataset(s) we will be querying.
// Repeat this call for multiple inputs.
req.dataset(
"covid19.canada.case-details",
"did:odf:fed01c8788dc7825dc95dfaa6c67f989b758d3ebcb1efcb9f47ea914470bd1f7f2bbb"
);
// Specify an arbitrary complex SQL query.
// Queries can include even JOINs
req.sql(
"with by_provice as ( "
" select "
" province, "
" count(*) as count "
" from 'covid19.canada.case-details' "
" group by 1 "
"), "
"ranked as ( "
" select "
" row_number() over (order by count desc) as rank, "
" province, "
" count "
" from by_provice "
") "
"select "
" province, "
" count "
"from ranked where rank = 1"
);
// Send request to the oracle and specify a callback
oracle.sendRequest(req, this.onResult);
}
// This function will be called by the oracle when
// request is fulfilled by some data provider
function onResult(OdfResponse.Res memory result) external {
// Important: Make sure only the ODF oracle can supply you data
require(msg.sender == address(oracle), "Can only be called by oracle");
// We expect only one row: [province, totalCases]
require(result.numRecords() == 1, "Expected one record");
CborReader.CBOR[] memory record = result.record(0);
province = record[0].readString();
totalCases = record[1].readInt();
}
}
Getting Started
Start by installing @opendatafabric/contracts
package that contains oracle interface and request/response types.
npm i @opendatafabric/contracts
See the example above for the recommended way of forming requests and decoding results.
More examples, documentation, and tests can be found in the contracts repository.
Supported Networks
Network | Contract Address | Oracle Fee |
---|---|---|
Ethereum Sepolia Testnet | 0xf1960569d1b4a23c34109d9341af6496ed90c0c3 | 0 |
BNB Smart Chain Testnet | 0x83857865971e941933dd36ebbf9475a867f67ca6 | 0 |
Ethereum Mainnet | coming soon | 0 |
Optimism | coming soon | 0 |
ODF oracle can be deployed on any EVM-compatible chain - you can request network support by filing an issue on the contracts repository.
Oracle contract and all providers currently operate with zero fees.
Verifiability
Every response submitted to the oracle by data providers includes special data that allows the query to be reproduced later against the same exact version of input data.
In REST API semantics an oracle request that looks like this:
{
"query": "select offset, city, population from populations order by offset desc",
"queryDialect": "SqlDataFusion",
"datasets": [{
"id": "did:odf:fed0119d20360650afd3d412c6b11529778b784c697559c0107d37ee5da61465726c4",
"alias": "populations"
}],
"dataFormat": "JsonAoA",
"include": ["input"],
}
Will be given a result such as:
{
"input": {
"query": "select offset, city, population from populations order by offset desc",
"queryDialect": "SqlDataFusion",
"dataFormat": "JsonAoA",
"include": ["Input"],
"datasets": [{
"id": "did:odf:fed0119d20360650afd3d412c6b11529778b784c697559c0107d37ee5da61465726c4",
"alias": "populations",
"blockHash": "f1620708557a44c88d23c83f2b915abc10a41cc38d2a278e851e5dc6bb02b7e1f9a1a"
}]
},
"output": {
"data": [
[1, "B", 200],
[0, "A", 100]
],
"dataFormat": "JsonAoa"
}
}
The input.datasets
block of the response contains state information that associates every input dataset with the hash of the last block that was considered during the query.
Whenever a provider supplies a response - this state information is recorded in the events emitted by the oracle contract on the blockchain.
This data acts as a commitment using which a data consumer can dispute results of a query. If another party repeats the same computations using the state data and arrives at different result - this means that the original provider has acted maliciously. See commitments documentation for details.
The reproducibility and verifiability of queries therefore make ODF the first of a kind optimistic oracle where:
- Queries can be fulfilled fast and cheap just by one provider
- Without going through a complex consensus mechanism
- While validity of data can still be ensured through the dispute resolution and sampling mechanisms along with staking and slashing.
Commitment Properties
Using the criteria discussed in the commitments documentation the oracle commitment can be described as follows:
- Hiding - NO
- The entire request and response data is publicly available, but this as an inherent limitation of non-ZK blockchains
- Binding - YES
- All inputs and outputs are immutably stored on-chain
- Succinct - YES*
- All response data ends up stored which we cannot avoid, but the commitment itself does not add much overhead
- Deterministic - YES
- Reproducible - YES
- Original query with all of its parameters is stored on-chain and can be repeated against the state information in response
- Non-repudiable - YES
- Automatically achieved as the provider signs the response transaction with its private key
- Composable - YES
- Through recursive nature of the commitment
Troubleshooting
To better understand what oracle providers return find the incoming oracle transaction in the block explorer e.g. 0x0f3a..4ff6.
Find the ProvideResults
log:
Copy the data field and paste it in https://cbor.me/ to get something like this:
The first three fields are: version
, success
, data
. Refer to OdfResponse
to understand the layout of the other fields.
Once you have one oracle response - you can use it as input in your unit tests to fully debug the decoding and processing logic without constantly re-deploying the contract and spending a lot of gas.
Here’s an example of one such tests in foundry
:
function testResponseDecoding() public pure {
bytes memory responseBytes = hex"8501F58A826..92AFE32";
OdfResponse.Res memory res = OdfResponse.fromBytes(0, responseBytes);
assertEq(res.numRecords(), 10);
CborReader.CBOR[] memory record = res.record(0);
string memory voiceId = record[0].readString();
int256 playtimeTotal = record[1].readInt();
assertEq(voiceId, "lana-del-rey");
assertEq(playtimeTotal, 6449);
}
Registering Your Own Provider
🚧 Under construction 🚧 - contact us if you’d like to use ODF oracle contract along with your private ODF node.