Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions erazhu31/TLS Notary DOracle/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM node:11-alpine

COPY src/oracle.js /src/oracle.js


RUN npm i https ethers fs get-ssl-certificate
ENTRYPOINT ["node", "src/oracle.js"]


6 changes: 6 additions & 0 deletions erazhu31/TLS Notary DOracle/smart-contract/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
build

package-lock.json
yarn.lock

75 changes: 75 additions & 0 deletions erazhu31/TLS Notary DOracle/smart-contract/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
iExec Dencentralized Oracle System
==================================

About iExec
-----------

Thanks to iExec, it is possible to achieve onchain consensus about the result of an offchain application. Applications are represented by smart contracts and execution results can be made available onchain with all the necessary proof.

Building an oracle application
------------------------------

iExec applications produce different outputs.
* The consensus is achieved based on a deterministic value describing the application output. By default this is the hash of the result archive, but can be overriden by the content of `/iexec_out/determinism.iexec`. Upon successful verification, this is stored onchain in the `task.resultDigest` field.
* The actual result. By default this is the IPFS address of a (potentially encrypted) archive containing the outputs, but can be overridden by the content of `/iexec_out/callback.iexec`. Upon successful verification, this is stored onchain in the `task.results` field.

An iExec oracle application such as the one used in the price-oracle example uses these 2 elements to produce verified results to the blockchain.

Given a set of parameters, the application produces a self-describing result, encodes it in a way that can be interpreted onchain, stores it in `/iexec_out/callback.iexec` so that is can be accessed onchain, and stores the hash of this encoded value to perform the consensus.

For example, given the parameters "BTC USD 9 2019-04-11T13:08:32.605Z" the price-oracle application will:

1. Retreive the price of BTC in USD at 2019-04-11T13:08:32.605Z
2. Multiply this value by 10e9 (to capture price value more accurately as it will be represented by an integer onchain)
3. encode the date, the description ("btc-usd-9") and the value using `abi.encode`
4. Store this result in `/iexec_out/callback.iexec`
5. hash the result and store it in `/iexec_out/determinism.iexec`

iExec will then achieve PoCo consensus on the `/iexec_out/determinism.iexec` value, and will store both the `/iexec_out/determinism.iexec` and the `/iexec_out/callback.iexec` onchain.

Given a taskID, it is possible to retrieve all the details of the computation as described above. The oracle smart contract just needs to retrieve the information, verify the validity of the execution and process the encoded result. Thanks to the PoCo consensus, anyone can require a computation and ask the oracle to update itself in a trustless manner.

How to setup an oracle contract
-------------------------------

1. Record the address of the iExec Hub and Clerk contracts

2. Register the requirements needed for a result to be processed
* Which application (single, any, whitelist?)
* Which dataset (single, any, whitelist?)
* Which workerpool (single, any, whitelist?)
* Minimum level of trust
* Mandatory tag

How to update an oracle contract
--------------------------------

1. Send the taskID of a valid execution to the oracle smart contract.
2. The oracle smart contract retrieves details about this task from the iexec's smart contracts.
3. The oracle smart contract verifies the execution is valid (authorized app, dataset, workerpool, trust level and tags).
4. The oracle smart contract verifies the hash of the results correspond to the resultDigest that achieved consensus, thus verifying the validity of the result field.
5. The oracle smart contract decodes the results using `abi.decode`.
6. The oracle smart contract processes the results. In the case of the price oracle, this means storing the value if it is more recent than the one currently recorded.

How to read price from the iExec price oracle
---------------------------------------------

Just query the oracle `values` field with the id of the requested field. For example, to get the most recent price of BTC in USD with 9 place precision (as described above), query `values(keccak256(bytes("BTC-USD-9")))` and this will return a structure containing the value, the associate date, and the details of the request.

Deployed addresses
------------------

1. **Kovan:**

price oracle: `https://kovan.etherscan.io/address/0x3b9f1a9aecb1991f3818f45bd4cc735f4bee93ac`

app whitelist: `https://kovan.etherscan.io/address/0x651a09cdff5a6669ea8bf05be11eff4aa9cbfdaf`

whitelist contains:

* `0xf92f39545340ce2fd6f4248a689fca4f660ae42f`
* `0xe01bccbcab54c42f999b6ce88d63d3a5e96cfdb7`

Whitelist is administered by:

* `0x7bd4783FDCAD405A28052a0d1f11236A741da593`
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
pragma solidity ^0.5.8;

import "openzeppelin-solidity/contracts/ownership/Ownable.sol";

contract Migrations is Ownable
{
uint256 public lastCompletedMigration;

constructor()
public
{
}

function setCompleted(uint completed) public onlyOwner
{
lastCompletedMigration = completed;
}

function upgrade(address newAddress) public onlyOwner
{
Migrations upgraded = Migrations(newAddress);
upgraded.setCompleted(lastCompletedMigration);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
pragma solidity ^0.5.8;
pragma experimental ABIEncoderV2;

import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "iexec-doracle-base/contracts/IexecDoracle.sol";

contract TlsNotaryOracle is Ownable, IexecDoracle
{
struct TimedValue
{
bytes32 oracleCallID;
string result;
}

mapping(bytes32 => TimedValue) public values;

event ValueUpdated(
bytes32 indexed id,
bytes32 indexed oracleCallID,
string oldResult,
string newResult
);

// Use _iexecHubAddr to force use of custom iexechub, leave 0x0 for autodetect
constructor(address _iexecHubAddr)
public IexecDoracle(_iexecHubAddr)
{}

function updateEnv(
address _authorizedApp
, address _authorizedDataset
, address _authorizedWorkerpool
, bytes32 _requiredtag
, uint256 _requiredtrust
)
public onlyOwner
{
_iexecDoracleUpdateSettings(_authorizedApp, _authorizedDataset, _authorizedWorkerpool, _requiredtag, _requiredtrust);
}

function decodeResults(bytes memory results)
public pure returns(string memory)
{ return abi.decode(results, (string)); }

function processResult(bytes32 _oracleCallID)
public
{
string memory result;

// Parse results
result = decodeResults(_iexecDoracleGetVerifiedResult(_oracleCallID));

// Process results
bytes32 id = keccak256(bytes(result));
emit ValueUpdated(id, _oracleCallID, values[id].result, result);
values[id].oracleCallID = _oracleCallID;
values[id].result = result;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes no sens. The whole purpose of an oracle is to get a unknown result. If the result is stored at keccak256(result) then you have to know the hash to access the value.

The ValueUpdated made sens in the context of the example (pricefeed) where an index can be updated. Are you expecting for values[id].result to be non-null it would have to contain a string which hash is the same as result's ...

25 changes: 25 additions & 0 deletions erazhu31/TLS Notary DOracle/smart-contract/daemon/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
FROM node:8-alpine

# changing user
USER root

# add necessary packages
RUN apk add --no-cache git python make g++

# create a work directory inside the container
RUN mkdir /app
WORKDIR /app

# copy project files
COPY . .

# install utilities
RUN npm install -g yarn ts-node typescript

# install dependencies
RUN yarn

# making entrypoint executable
RUN chmod +x entrypoint.sh

ENTRYPOINT ["./entrypoint.sh"]
145 changes: 145 additions & 0 deletions erazhu31/TLS Notary DOracle/smart-contract/daemon/daemon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { ethers } from 'ethers';
import * as utils from './utils';

const IexecHub = require('iexec-poco/build/contracts/IexecHub.json');
const IexecClerk = require('iexec-poco/build/contracts/IexecClerk.json');
const IERC734 = require('iexec-poco/build/contracts/IERC734.json');
// const PriceOracle = require('../build/contracts/PriceOracle.json');

// Mainnet & Kovan deployment use the old ABI
const PriceOracle = { abi:[{"constant":true,"inputs":[{"name":"_identity","type":"address"},{"name":"_hash","type":"bytes32"},{"name":"_signature","type":"bytes"}],"name":"verifySignature","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_authorizedApp","type":"address"},{"name":"_authorizedDataset","type":"address"},{"name":"_authorizedWorkerpool","type":"address"},{"name":"_requiredtag","type":"bytes32"},{"name":"_requiredtrust","type":"uint256"}],"name":"updateEnv","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"m_authorizedApp","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"results","type":"bytes"}],"name":"decodeResults","outputs":[{"name":"","type":"uint256"},{"name":"","type":"string"},{"name":"","type":"uint256"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"name":"_doracleCallId","type":"bytes32"},{"name":"","type":"bytes"}],"name":"receiveResult","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"renounceOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"m_authorizedDataset","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isOwner","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"m_iexecClerk","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"m_iexecHub","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"m_authorizedWorkerpool","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"values","outputs":[{"name":"oracleCallID","type":"bytes32"},{"name":"date","type":"uint256"},{"name":"value","type":"uint256"},{"name":"details","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"m_requiredtrust","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_oracleCallID","type":"bytes32"}],"name":"processResult","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"m_requiredtag","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_iexecHubAddr","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"bytes32"},{"indexed":true,"name":"oracleCallID","type":"bytes32"},{"indexed":false,"name":"oldDate","type":"uint256"},{"indexed":false,"name":"oldValue","type":"uint256"},{"indexed":false,"name":"newDate","type":"uint256"},{"indexed":false,"name":"newValue","type":"uint256"}],"name":"ValueChange","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"doracleCallId","type":"bytes32"}],"name":"ResultReady","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previousOwner","type":"address"},{"indexed":true,"name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"}]};

export default class Daemon
{
address: string;
wallet: ethers.Wallet;
doracle: ethers.Contract;
iexechub: ethers.Contract;
iexecclerk: ethers.Contract;
requester: string;

settings:
{
authorizedApp: string,
authorizedDataset: string,
authorizedWorkerpool: string,
requiredtag: string,
requiredtrust: number,
GROUPMEMBER_PURPOSE: number,
};

constructor(address: string, wallet: ethers.Wallet, requester: string = null)
{
this.address = address;
this.wallet = wallet;
this.requester = requester;
}

async start(listener: boolean = true) : Promise<void>
{
console.log(`Connecting to contracts`);
this.doracle = new ethers.Contract(this.address, PriceOracle.abi, this.wallet);
console.log(`- doracle ${this.doracle.address}`);
this.iexechub = new ethers.Contract(await this.doracle.m_iexecHub(), IexecHub.abi, this.wallet.provider);
console.log(`- iexechub ${this.iexechub.address}`);
this.iexecclerk = new ethers.Contract(await this.doracle.m_iexecClerk(), IexecClerk.abi, this.wallet.provider);
console.log(`- iexecclerk ${this.iexecclerk.address}`);

console.log(`Retrieving doracle settings:`);
this.settings = {
authorizedApp: await this.doracle.m_authorizedApp(),
authorizedDataset: await this.doracle.m_authorizedDataset(),
authorizedWorkerpool: await this.doracle.m_authorizedWorkerpool(),
requiredtag: await this.doracle.m_requiredtag(),
requiredtrust: await this.doracle.m_requiredtrust(),
GROUPMEMBER_PURPOSE: await this.iexecclerk.GROUPMEMBER_PURPOSE(),
}
console.log(`- authorizedApp: ${this.settings.authorizedApp}` );
console.log(`- authorizedDataset: ${this.settings.authorizedDataset}` );
console.log(`- authorizedWorkerpool: ${this.settings.authorizedWorkerpool}`);
console.log(`- requiredtag: ${this.settings.requiredtag}` );
console.log(`- requiredtrust: ${this.settings.requiredtrust}` );
console.log(`- GROUPMEMBER_PURPOSE: ${this.settings.GROUPMEMBER_PURPOSE}` );

if (listener)
{
console.log(`Starting event listener.`)
this.doracle.on("ResultReady(bytes32)", this.trigger.bind(this));
console.log(`====== Daemon is running ======`);
}
else
{
console.log(`====== Daemon is ready ======`);
}
}

async checkIdentity(identity: string, candidate: string, purpose: number): Promise<boolean>
{
try
{
return identity == candidate || await (new ethers.Contract(identity, IERC734.abi, this.wallet.provider)).keyHasPurpose(utils.addrToKey(candidate), purpose);
}
catch
{
console.log(identity, candidate)
return false;
}
}

async getVerifiedResult(doracleCallId: string) : Promise<string>
{
let task = await this.iexechub.viewTask(doracleCallId);
let deal = await this.iexecclerk.viewDeal(task.dealid);

if (this.requester)
{
utils.require(deal.requester == this.requester, "requester filtered (this is not an error)");
}

utils.require(task.status == 3, "result-not-available");
utils.require(task.resultDigest == ethers.utils.keccak256(task.results), "result-not-validated-by-consensus");
utils.require(this.settings.authorizedApp == ethers.constants.AddressZero || await this.checkIdentity(this.settings.authorizedApp, deal.app.pointer, this.settings.GROUPMEMBER_PURPOSE), "unauthorized-app");
utils.require(this.settings.authorizedDataset == ethers.constants.AddressZero || await this.checkIdentity(this.settings.authorizedDataset, deal.dataset.pointer, this.settings.GROUPMEMBER_PURPOSE), "unauthorized-dataset");
utils.require(this.settings.authorizedWorkerpool == ethers.constants.AddressZero || await this.checkIdentity(this.settings.authorizedWorkerpool, deal.workerpool.pointer, this.settings.GROUPMEMBER_PURPOSE), "unauthorized-workerpool");
utils.require(this.settings.requiredtrust <= deal.trust, "invalid-trust");

// Check tag - must be done byte by byte.
let [ ta, rta ] = [ deal.tag, this.settings.requiredtag ].map(ethers.utils.arrayify);
for (var i in ta) utils.require((rta[i] & ~ta[i]) == 0, "invalid-tag");

return task.results;
}

async checkData(data: string) : Promise<void>
{
let [ date, details, value ] = ethers.utils.defaultAbiCoder.decode(["uint256", "string", "uint256"], data);
let entry = await this.doracle.values(ethers.utils.solidityKeccak256(["string"],[details]));
utils.require(entry.date < date, "new-value-is-too-old");
}

trigger(doracleCallId: string, event: {})
{
process.stdout.write(`${new Date().toISOString()} | processing ${doracleCallId} ... `);
this.getVerifiedResult(doracleCallId)
.then(data => {
this.checkData(data)
.then(() => {
this.doracle.processResult(doracleCallId)
.then(tx => {
process.stdout.write(`success\n`);
})
.catch(e => {
const txHash = e.transactionHash;
const data = e.data[txHash];
process.stdout.write(`Error: ${data.error} (${data.reason})\n`);
});
})
.catch(reason => {
process.stdout.write(`Invalid results (${reason})\n`);
});
})
.catch(reason => {
process.stdout.write(`Failled to verify results (${reason})\n`);
});
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file makes sens for the pricefeed, but is completelly useless here. Either update it to you oracle or remove it!

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: '2'
services:
doracle-daemon:
image: iexechub/iexec-doracle-daemon:latest
environment:
- DORACLE_ADDR=xxx
- MNEMONIC=xxxx
- PROVIDER=xxx
- REQUESTER=xxxx
restart: unless-stopped
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
echo "[INFO] Launching DOracle Daemon"
/usr/local/bin/ts-node launch.ts
14 changes: 14 additions & 0 deletions erazhu31/TLS Notary DOracle/smart-contract/daemon/launch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ethers } from 'ethers';
import Daemon from './daemon';

// mainnet 0xed4a0189511859427c33dcc7c85fdd36575ae946
// kovan 0x3b9F1a9aeCb1991f3818f45bd4CC735f4BEE93Ac

let doracle_addr: string = process.env.DORACLE_ADDR;
let private_key: string = process.env.MNEMONIC;
let provider: ethers.providers.Provider = ethers.getDefaultProvider(process.env.PROVIDER);

let wallet: ethers.Wallet = new ethers.Wallet(private_key, provider);
let daemon: Daemon = new Daemon(doracle_addr, wallet, process.env.REQUESTER);

daemon.start();
Loading