diff --git a/.DS_Store b/.DS_Store index 89183c66..61309821 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/flight-delay/.DS_Store b/flight-delay/.DS_Store new file mode 100644 index 00000000..4dc40adc Binary files /dev/null and b/flight-delay/.DS_Store differ diff --git a/flight-delay/app/Dockerfile b/flight-delay/app/Dockerfile new file mode 100644 index 00000000..aac1ee4e --- /dev/null +++ b/flight-delay/app/Dockerfile @@ -0,0 +1,7 @@ +FROM node:11-alpine +COPY flightDelay.js /src/flightDelay.js +COPY entrypoint.sh /entrypoint.sh +RUN npm i axios ethers fs +RUN mkdir iexec_out +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/flight-delay/app/entrypoint.sh b/flight-delay/app/entrypoint.sh new file mode 100644 index 00000000..cb36ad47 --- /dev/null +++ b/flight-delay/app/entrypoint.sh @@ -0,0 +1,2 @@ +#!/bin/sh +node src/flightDelay.js $@ \ No newline at end of file diff --git a/flight-delay/app/flightDelay.js b/flight-delay/app/flightDelay.js new file mode 100644 index 00000000..f972caa1 --- /dev/null +++ b/flight-delay/app/flightDelay.js @@ -0,0 +1,60 @@ +const ethers = require('ethers'); +const fs = require('fs'); +const axios = require('axios'); + + +const root = 'iexec_out'; +const determinismFilePath = `${root}/determinism.iexec`; +const callbackFilePath = `${root}/callback.iexec`; +const errorFilePath = `${root}/error.iexec`; + + +/***************************************************************************** + * ARGUMENTS * + *****************************************************************************/ + +var [id, timestamp] = process.argv.slice(2).map(s => s); + +var appId = '0ddf43fd'; +var appKey = 'c8663643d1b8d907717810f38f97ffb1'; + +// var timestampInMili = new Date().getTime(); +// var timestamp = parseInt(timestampInMili/1000) +// console.log(longi); +// console.log(lati); +// console.log(timestamp); +//flightID = 1023084433 +/***************************************************************************** + * HTTP QUERY * + *****************************************************************************/ + +let url = `https://api.flightstats.com/flex/flightstatus/rest/v2/json/flight/status/${id}?appId=${appId}&appKey=${appKey}`; +/***************************************************************************** + * EXECUTE * + *****************************************************************************/ +axios.get(url) +.then(res=>{ + // console.log(res); + var result = res.data; + var delay = parseInt(result.flightStatus.delays.departureGateDelayMinutes); + console.log(delay); + var iexeccallback = ethers.utils.defaultAbiCoder.encode(['uint256', 'uint256', 'uint256'], [timestamp, id, delay]); + var iexecconsensus = ethers.utils.keccak256(iexeccallback); + fs.writeFile(callbackFilePath, iexeccallback , (err) => {}); + fs.writeFile(determinismFilePath, iexecconsensus, (err) => {}); + +}) +.catch(err=>{ + fs.writeFile( + errorFilePath, + err.toString(), + (error) => {} + ); + fs.writeFile( + determinismFilePath, + ethers.utils.solidityKeccak256(['string'],[err.toString()]), + (error) => {} + ); + throw new Error(err); +}) + diff --git a/flight-delay/chain.json b/flight-delay/chain.json new file mode 100644 index 00000000..700ab0e4 --- /dev/null +++ b/flight-delay/chain.json @@ -0,0 +1,29 @@ +{ + "default": "kovan", + "chains": { + "dev": { + "host": "http://localhost:8545", + "sms": "http://localhost:5000", + "id": "17", + "hub": "0x60E25C038D70A15364DAc11A042DB1dD7A2cccBC" + }, + "ropsten": { + "host": "https://ropsten.infura.io/v3/f3e0664e01504f5ab2b4360853ce0dc7", + "id": "3" + }, + "rinkeby": { + "host": "https://rinkeby.infura.io/v3/f3e0664e01504f5ab2b4360853ce0dc7", + "id": "4" + }, + "kovan": { + "host": "https://kovan.infura.io/v3/f3e0664e01504f5ab2b4360853ce0dc7", + "id": "42", + "sms": "https://sms-kovan.iex.ec" + }, + "mainnet": { + "host": "https://mainnet.infura.io/v3/f3e0664e01504f5ab2b4360853ce0dc7", + "id": "1", + "sms": "https://sms-mainnet.iex.ec" + } + } +} \ No newline at end of file diff --git a/flight-delay/deployed.json b/flight-delay/deployed.json new file mode 100644 index 00000000..19f87fcc --- /dev/null +++ b/flight-delay/deployed.json @@ -0,0 +1,5 @@ +{ + "app": { + "42": "0x2bEe1A8d963D9f3d8Da6Db5A5e23aA4dB537811B" + } +} \ No newline at end of file diff --git a/flight-delay/iexec.json b/flight-delay/iexec.json new file mode 100644 index 00000000..18294047 --- /dev/null +++ b/flight-delay/iexec.json @@ -0,0 +1,35 @@ +{ + "description": "My iExec ressource description, must be at least 150 chars long in order to pass the validation checks. Describe your application, dataset or workerpool to your users", + "license": "MIT", + "author": "?", + "social": { + "website": "?", + "github": "?" + }, + "logo": "logo.png", + "buyConf": { + "params": "", + "tag": "0x0000000000000000000000000000000000000000000000000000000000000000", + "trust": "0", + "callback": "0x0000000000000000000000000000000000000000" + }, + "app": { + "owner": "0xf3Ba773Af9e30B8187E2Abbd0B4F3218273127D0", + "name": "flightDelay", + "type": "DOCKER", + "multiaddr": "registry.hub.docker.com/nikhil3000/flight-delay:1.0.1", + "checksum": "0x866f12aee5381b2ab2b029b678f0c2dbc125ee06db7f045eeaefcb0998a589f5", + "mrenclave": "" + }, + "order": { + "apporder": { + "app": "0x2bEe1A8d963D9f3d8Da6Db5A5e23aA4dB537811B", + "appprice": "0", + "volume": "1000000", + "tag": "0x0000000000000000000000000000000000000000000000000000000000000000", + "datasetrestrict": "0x0000000000000000000000000000000000000000", + "workerpoolrestrict": "0x0000000000000000000000000000000000000000", + "requesterrestrict": "0x0000000000000000000000000000000000000000" + } + } +} \ No newline at end of file diff --git a/flight-delay/orders.json b/flight-delay/orders.json new file mode 100644 index 00000000..2d7438d5 --- /dev/null +++ b/flight-delay/orders.json @@ -0,0 +1,15 @@ +{ + "42": { + "apporder": { + "app": "0x2bEe1A8d963D9f3d8Da6Db5A5e23aA4dB537811B", + "appprice": "0", + "volume": "1000000", + "tag": "0x0000000000000000000000000000000000000000000000000000000000000000", + "datasetrestrict": "0x0000000000000000000000000000000000000000", + "workerpoolrestrict": "0x0000000000000000000000000000000000000000", + "requesterrestrict": "0x0000000000000000000000000000000000000000", + "salt": "0x0285178da87007386921c7dc76fc009fa47ac4ab76986949d3234c6494c909cd", + "sign": "0xc2097916b26007f2b26a824aa4690901d86c79e21158bae2426054397474c0384ddf3c3be20ff065a2b64c96cb02af4ef25f7f0d6213704db98c769c6875a51c1c" + } + } +} \ No newline at end of file diff --git a/flight-delay/smart-contract/.DS_Store b/flight-delay/smart-contract/.DS_Store new file mode 100644 index 00000000..212c5412 Binary files /dev/null and b/flight-delay/smart-contract/.DS_Store differ diff --git a/flight-delay/smart-contract/.gitignore b/flight-delay/smart-contract/.gitignore new file mode 100644 index 00000000..20d2fe75 --- /dev/null +++ b/flight-delay/smart-contract/.gitignore @@ -0,0 +1,6 @@ +node_modules +build + +package-lock.json +yarn.lock + diff --git a/flight-delay/smart-contract/README.md b/flight-delay/smart-contract/README.md new file mode 100644 index 00000000..893dfb85 --- /dev/null +++ b/flight-delay/smart-contract/README.md @@ -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` diff --git a/flight-delay/smart-contract/contracts/FlightDelayOracle.sol b/flight-delay/smart-contract/contracts/FlightDelayOracle.sol new file mode 100644 index 00000000..7fc86bfd --- /dev/null +++ b/flight-delay/smart-contract/contracts/FlightDelayOracle.sol @@ -0,0 +1,68 @@ +pragma solidity ^0.5.8; +pragma experimental ABIEncoderV2; + +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import "iexec-doracle-base/contracts/IexecDoracle.sol"; + +contract FlightDelayOracle is Ownable, IexecDoracle +{ + struct TimedValue + { + bytes32 oracleCallID; + uint256 date; + uint256 flightId; + uint256 delay; //delay in minutes + } + + mapping(bytes32 => TimedValue) public values; + + event ValueUpdated( + bytes32 indexed id, + bytes32 indexed oracleCallID, + uint256 oldDate, + uint256 oldDelay, + uint256 newDate, + uint256 newDelay + ); + + // 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(uint256, uint256 , uint256) + { return abi.decode(results, (uint256, uint256 , uint256 )); } + + function processResult(bytes32 _oracleCallID) + public + { + uint256 date; + uint256 flightId; + uint256 delay; //correct to 3 decimal places + + // Parse results + (date, flightId, delay) = decodeResults(_iexecDoracleGetVerifiedResult(_oracleCallID)); + + // Process results + bytes32 id = keccak256(abi.encode(flightId)); + require(values[id].date < date, "new-value-is-too-old"); + emit ValueUpdated(id, _oracleCallID, values[id].date, values[id].delay, date, delay); + values[id].oracleCallID = _oracleCallID; + values[id].date = date; + values[id].flightId = flightId; + values[id].delay = delay; + } +} diff --git a/flight-delay/smart-contract/contracts/Migrations.sol b/flight-delay/smart-contract/contracts/Migrations.sol new file mode 100644 index 00000000..85e11e6e --- /dev/null +++ b/flight-delay/smart-contract/contracts/Migrations.sol @@ -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); + } +} diff --git a/flight-delay/smart-contract/daemon/Dockerfile b/flight-delay/smart-contract/daemon/Dockerfile new file mode 100644 index 00000000..741c1478 --- /dev/null +++ b/flight-delay/smart-contract/daemon/Dockerfile @@ -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"] diff --git a/flight-delay/smart-contract/daemon/daemon.ts b/flight-delay/smart-contract/daemon/daemon.ts new file mode 100644 index 00000000..78f7fe6b --- /dev/null +++ b/flight-delay/smart-contract/daemon/daemon.ts @@ -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 + { + 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 + { + 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 + { + 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 + { + 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`); + }); + } +} diff --git a/flight-delay/smart-contract/daemon/docker-compose.yml b/flight-delay/smart-contract/daemon/docker-compose.yml new file mode 100644 index 00000000..44a8ad9a --- /dev/null +++ b/flight-delay/smart-contract/daemon/docker-compose.yml @@ -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 diff --git a/flight-delay/smart-contract/daemon/entrypoint.sh b/flight-delay/smart-contract/daemon/entrypoint.sh new file mode 100644 index 00000000..9a2c9d0a --- /dev/null +++ b/flight-delay/smart-contract/daemon/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/sh +echo "[INFO] Launching DOracle Daemon" +/usr/local/bin/ts-node launch.ts diff --git a/flight-delay/smart-contract/daemon/launch.ts b/flight-delay/smart-contract/daemon/launch.ts new file mode 100644 index 00000000..1896f256 --- /dev/null +++ b/flight-delay/smart-contract/daemon/launch.ts @@ -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(); diff --git a/flight-delay/smart-contract/daemon/package.json b/flight-delay/smart-contract/daemon/package.json new file mode 100644 index 00000000..20757f0b --- /dev/null +++ b/flight-delay/smart-contract/daemon/package.json @@ -0,0 +1,31 @@ +{ + "name": "price-feed-doracle", + "version": "0.0.1", + "description": "Contracts and tools for the iExec based price feed DOracle", + "repository": { + "type": "git", + "url": "git+https://github.com/iExecBlockchainComputing/price-feed-doracle.git" + }, + "bugs": { + "url": "https://github.com/iExecBlockchainComputing/price-feed-doracle.git/issues" + }, + "files": [ + "/build", + "/contracts", + "daemon" + ], + "author": "iExec", + "license": "ISC", + "homepage": "https://github.com/iExecBlockchainComputing/price-feed-doracle.git#readme", + "dependencies": { + "chai": "^4.2.0", + "ethereumjs-util": "^5.2.0", + "iexec-doracle-base": "^0.0.4", + "iexec-poco": "^3.0.35", + "iexec-solidity": "^0.0.7", + "multiaddr": "^6.0.6", + "openzeppelin-solidity": "^2.2.0", + "openzeppelin-test-helpers": "^0.1.5", + "rlc-faucet-contract": "^2.0.0" + } +} diff --git a/flight-delay/smart-contract/daemon/utils/addrToKey.ts b/flight-delay/smart-contract/daemon/utils/addrToKey.ts new file mode 100644 index 00000000..a194f9af --- /dev/null +++ b/flight-delay/smart-contract/daemon/utils/addrToKey.ts @@ -0,0 +1,6 @@ +import { ethers } from 'ethers'; + +export default function(addr: string) : string +{ + return ethers.utils.hexZeroPad(addr, 32).toString().toLowerCase(); +} diff --git a/flight-delay/smart-contract/daemon/utils/index.ts b/flight-delay/smart-contract/daemon/utils/index.ts new file mode 100644 index 00000000..a9767469 --- /dev/null +++ b/flight-delay/smart-contract/daemon/utils/index.ts @@ -0,0 +1,2 @@ +export { default as require } from "./require"; +export { default as addrToKey } from "./addrToKey"; diff --git a/flight-delay/smart-contract/daemon/utils/require.ts b/flight-delay/smart-contract/daemon/utils/require.ts new file mode 100644 index 00000000..56eadba2 --- /dev/null +++ b/flight-delay/smart-contract/daemon/utils/require.ts @@ -0,0 +1,4 @@ +export default function(value: boolean, reason: string = "") : void +{ + if (!value) throw Error(reason); +} diff --git a/flight-delay/smart-contract/migrations/1_initial_migration.js b/flight-delay/smart-contract/migrations/1_initial_migration.js new file mode 100644 index 00000000..66673a2e --- /dev/null +++ b/flight-delay/smart-contract/migrations/1_initial_migration.js @@ -0,0 +1,5 @@ +var Migrations = artifacts.require("./tools/Migrations.sol"); + +module.exports = function(deployer) { + deployer.deploy(Migrations, {gas: 500000}); +}; diff --git a/flight-delay/smart-contract/migrations/2_deploy_contracts.js b/flight-delay/smart-contract/migrations/2_deploy_contracts.js new file mode 100644 index 00000000..af37388e --- /dev/null +++ b/flight-delay/smart-contract/migrations/2_deploy_contracts.js @@ -0,0 +1,8 @@ +var FlightDelayOracle = artifacts.require("FlightDelayOracle"); + +module.exports = async function(deployer, network, accounts) +{ + await deployer.deploy(FlightDelayOracle, "0x0000000000000000000000000000000000000000", { gas: 2500000 }); + FlightDelayOracleInstance = await FlightDelayOracle.deployed(); + console.log("FlightDelayOracle deployed at address: " + FlightDelayOracleInstance.address); +}; diff --git a/flight-delay/smart-contract/package.json b/flight-delay/smart-contract/package.json new file mode 100644 index 00000000..cecad941 --- /dev/null +++ b/flight-delay/smart-contract/package.json @@ -0,0 +1,28 @@ +{ + "name": "price-feed-doracle", + "version": "0.0.1", + "description": "Contracts and tools for the iExec based price feed DOracle", + "repository": { + "type": "git", + "url": "git+https://github.com/iExecBlockchainComputing/price-feed-doracle.git" + }, + "bugs": { + "url": "https://github.com/iExecBlockchainComputing/price-feed-doracle.git/issues" + }, + "files": [ + "/build", + "/contracts", + "daemon" + ], + "author": "iExec", + "license": "ISC", + "homepage": "https://github.com/iExecBlockchainComputing/price-feed-doracle.git#readme", + "dependencies": { + "iexec-doracle-base": "^0.0.6", + "iexec-poco": "^3.0.35", + "iexec-solidity": "^0.0.7", + "openzeppelin-solidity": "^2.2.0", + "truffle": "^5.0.25", + "truffle-hdwallet-provider": "^1.0.12" + } +} diff --git a/flight-delay/smart-contract/truffle.js b/flight-delay/smart-contract/truffle.js new file mode 100644 index 00000000..4f36088b --- /dev/null +++ b/flight-delay/smart-contract/truffle.js @@ -0,0 +1,62 @@ +var HDWalletProvider = require("truffle-hdwallet-provider"); + +module.exports = +{ + networks: + { + docker: + { + host: "iexec-geth-local", + port: 8545, + network_id: "*", // Match any network id, + gasPrice: 22000000000, //22Gwei + }, + development: + { + host: "localhost", + port: 8545, + network_id: "*", // Match any network id, + gasPrice: 22000000000, //22Gwei + }, + coverage: + { + host: "localhost", + port: 8555, // <-- If you change this, also set the port option in .solcover.js. + network_id: "*", + gas: 0xFFFFFFFFFFF, // <-- Use this high gas value + gasPrice: 0x01 // <-- Use this low gas price + }, + mainnet: + { + provider: () => new HDWalletProvider(process.env.MNEMONIC, "https://mainnet.infura.io/v3/f3e0664e01504f5ab2b4360853ce0dc7"), + network_id: '1', + gasPrice: 22000000000, //22Gwei + }, + ropsten: + { + provider: () => new HDWalletProvider(process.env.MNEMONIC, "https://ropsten.infura.io/v3/f3e0664e01504f5ab2b4360853ce0dc7"), + network_id: '3', + gasPrice: 22000000000, //22Gwei + }, + kovan: { + provider: () => new HDWalletProvider(process.env.MNEMONIC, "https://kovan.infura.io/v3/f3e0664e01504f5ab2b4360853ce0dc7"), + network_id: '42', + gasPrice: 1000000000, //1Gwei + } + }, + compilers: { + solc: { + version: "0.5.10", + settings: { + optimizer: { + enabled: true, + runs: 200 + } + } + } + }, + mocha: + { + enableTimeouts: false + } +};