TLDR: In this post, I'll explain how to build an Ethereum self-hosted blockchain oracle. This service will query APIs upon request by smart-contracts and add the information on the blockchain. The described approach allows for having multiple competing parties deploying the same oracle to have the same say in the result. But can easily be converted into a simple oracle to serve a single stakeholder.
Background
Smart Contracts in Ethereum can power a wide range of applications, however, and due to the nature of the blockchain, smart contracts lack an essential feature: Internet Connectivity.
Why? you may ask
The Ethereum blockchain is designed to be entirely deterministic, meaning that if someone downloads the whole network history and replays it they should always end up with the same state. Determinism is necessary so that nodes can come to a consensus.
However, the internet is not deterministic, a smart contract querying an API at a certain point in time, cannot be guaranteed that later querying the same API will get the same result. APIs and data on the web change. Therefore, by nature smart contracts lack connectivity.
So Oracles right?
The name oracle comes from the fact that oracles, historically speaking were sources of the truth. And that's what we need.
Oracles are services that insert data on the blockchain to be used by smart contract. By adding a transaction with the required information to the blockchain the smart contract can ran and always obtain the same information since it is retrieving it from the blockchain.
Solution
We will create an oracle service that can query JSON APIs and retrieve a single value from the API response. The oracle will save all the requests and answers and will have a predefined set of stakeholders.
These are the accounts running the node.js service that queries the APIs and returns a response to the oracle. The oracle also has a minimum number of equal responses that it must receive in order to confirm that the answer provided is valid.
This way, if competing parties depend on the oracle to power their contracts and one of the parties (nodes) becomes rogue or tries to manipulate the result, it can't because they agreed on a predefined quorum of equal answers.
Architecture
This oracle will comprise two components. The on-chain oracle (a smart contract) and the off-chain oracle service (node.js server).
The on-chain oracle is a smart-contract that has a public function, createRequest, that receives the URL, to query, and the attribute to retrieve. And then, launches an event to alert the off-chain oracle of a new request.
The off-chain oracle is composed of several node.js services deployed by different parties that will query the API and return to the contract the response.
The on-chain oracle then verifies if the minimum number of equal responses has been reached and if so emits an event saying that it has achieved consensus on the value so that the client smart-contract that queried the oracle knows that it has its response.
On-chain Oracle Implementation
We define the oracle contract with the agreed terms: Minimum Quorum and Total Oracles. For this example, there are three stakeholders, and for achieving consensus 2 out of 3 must provide the same answer.
pragma solidity >=0.4.21 <0.6.0;
contract Oracle {
Request[] requests; //list of requests made to the contract
uint currentId = 0; //increasing request id
uint minQuorum = 2; //minimum number of responses to receive before declaring final result
uint totalOracleCount = 3; // Hardcoded oracle count
}
Then we add the Request Struct, which will hold the requests:
// defines a general api request
struct Request {
uint id; //request id
string urlToQuery; //API url
string attributeToFetch; //json attribute (key) to retrieve in the response
string agreedValue; //value from key
mapping(uint => string) anwers; //answers provided by the oracles
mapping(address => uint) quorum; //oracles which will query the answer (1=oracle hasn't voted, 2=oracle has voted)
}
Now we can create the public function, createRequest, that client smart contracts (any contract that wants to use the oracle service) will call:
function createRequest (
string memory _urlToQuery,
string memory _attributeToFetch
)
public
{
uint lenght = requests.push(Request(currentId, _urlToQuery, _attributeToFetch, ""));
Request storage r = requests[lenght-1];
// Hardcoded oracles address
r.quorum[address(0x6c2339b46F41a06f09CA0051ddAD54D1e582bA77)] = 1;
r.quorum[address(0xb5346CF224c02186606e5f89EACC21eC25398077)] = 1;
r.quorum[address(0xa2997F1CA363D11a0a35bB1Ac0Ff7849bc13e914)] = 1;
// launch an event to be detected by oracle outside of blockchain
emit NewRequest (
currentId,
_urlToQuery,
_attributeToFetch
);
// increase request id
currentId++;
}
This function contains an important part of the agreement between the stakeholders. The addresses of the accounts that are trusted to take part in the final solution. And will emit the event, NewRequest that will be listened by the off-chain oracles.
//event that triggers oracle outside of the blockchain
event NewRequest (
uint id,
string urlToQuery,
string attributeToFetch
);
Upon listening to this event (I'll explain this part later bellow) the off-chain oracles will call the public function updateRequest.
//called by the oracle to record its answer
function updateRequest (
uint _id,
string memory _valueRetrieved
) public {
Request storage currRequest = requests[_id];
//check if oracle is in the list of trusted oracles
//and if the oracle hasn't voted yet
if(currRequest.quorum[address(msg.sender)] == 1){
//marking that this address has voted
currRequest.quorum[msg.sender] = 2;
//iterate through "array" of answers until a position is free and save the retrieved value
uint tmpI = 0;
bool found = false;
while(!found) {
//find first empty slot
if(bytes(currRequest.anwers[tmpI]).length == 0){
found = true;
currRequest.anwers[tmpI] = _valueRetrieved;
}
tmpI++;
}
uint currentQuorum = 0;
//iterate through oracle list and check if enough oracles(minimum quorum)
//have voted the same answer has the current one
for(uint i = 0; i < totalOracleCount; i++){
bytes memory a = bytes(currRequest.anwers[i]);
bytes memory b = bytes(_valueRetrieved);
if(keccak256(a) == keccak256(b)){
currentQuorum++;
if(currentQuorum >= minQuorum){
currRequest.agreedValue = _valueRetrieved;
emit UpdatedRequest (
currRequest.id,
currRequest.urlToQuery,
currRequest.attributeToFetch,
currRequest.agreedValue
);
}
}
}
}
}
This function will first check if the caller is one of the predefined addresses. Then it will check it the oracle hasn't voted and if so it will save the oracle answer. Then it will check if that answer has been provided by at least the required minimum quorum. If so, then we have agreed on a result and will emit an event, UpdatedRequest, to alert the client contract of the result.
//triggered when there's a consensus on the final result
event UpdatedRequest (
uint id,
string urlToQuery,
string attributeToFetch,
string agreedValue
);
HERE You can find the full oracle code and instructions on how to use truffle to create the boilerplate.
Off-chain Oracle Implementation
This is the simpler part, it's any service that can listen to the emitted blockchains and queries an API.
It listens to the events on the on-chain oracle, using web3, and queries the requested API, parses the retrieved JSON for the requested key and calls the public function updateRequest.
Since it is not the focus of the post and it's a simple service, I will not go further into the details of the implementation of the off-chain service.
HERE you can find the code for this service that should be deployed by all the stakeholders.
Summing up
This implementation allows to not depend on a single party to be the source of the truth, by being the only one querying the API, but to have multiple parties agreeing on a result. It is also a very flexible service since it can query any public JSON API, allowing to be used in a plethora of use cases.
All the code can be found HERE.