2017年10月26日

[Blockchain][Ethereum]建立oracle

Ethereum中,所有的資料都只在blockchain中流動,如果需要取得外部的資訊(例如天氣資料),只能透過oracle跟外部溝通。在這篇文章我們不討論oracle的安全性議題,而專注在如何建立一個oracle,讓外部取得blockchain中發生的事件。

完整的oracle contract架構如下:

  • myClientContract"繼承"OracleApp合約
  • OracleResolver合約保存實際Oracle合約(OracleContract)的位址
  • OracleContracttrigger一個QueryEvent
  • Offchain listener會監控QueryEvent,如果此Event發生,則Offchain listener會收到通知,在這裡便可以將資料存到外部資料庫

步驟

所有的source code都在Github上:https://github.com/michael-chi/blockchain-learning/tree/master/oracle

建立OracleContract.sol

pragma solidity ^0.4.10;

contract OracleContract4 {

    address owner;

   address public callbackAddress;

    string public state;

    event QueryEvent(bytes32 id, string query);

    function OracleContract4 () {

        owner = msg.sender;

    }

    modifier ownerOnly {

        require(owner == msg.sender);

        _;

    }

    function setCallbackAddress(address _callbackAddress) {

        callbackAddress = _callbackAddress;

    }

    function getRandomNumber() returns (uint) {

        return 12;

    }

    function query(string _query) returns (bytes32) {

        bytes32 id = sha3(block.number, now, _query, msg.sender);

        QueryEvent(id, _query);

        return id;

    }

}

其中query()這個function將提供給我們實際的合約呼叫;當此function被呼叫時,QueryEvent事件會被觸發,而我們的offchain listener會監聽這個事件。

建立OracleResolver.sol

pragma solidity ^0.4.10;

contract OracleResolver4 {

    address owner;

    address public oracleAddress;

    function OracleResolver4() {

        owner = msg.sender;

    }

    modifier onwerOnly {

        require(msg.sender == owner);

        _;

    }

    function setOracleAddress(address addr) {

        oracleAddress = addr;

    }

    function getOracleAddress() constant returns (address) {

        return oracleAddress;

    }

}

  • 建立完成後,我們要先將這兩個合約佈署到Ethereum上。這邊我們透過truffle來做佈署

n   如果先前沒有安裝truffle,可以在Azure Marketplace直接建立安裝好truffle的虛擬機器。

n   建立一個空的目錄後,執行truffle init webpack初始化truffle專案

n   將剛剛建立好的.sol檔案copycontracts目錄下

n   修改truffle.json檔案,將host改為你的Ethereum Transaction Node IP

n   執行truffle compile建置.sol檔案

n   建置完成後,會在目錄下建立一個新的build目錄,其下會有一個contracts目錄;在目錄中可以看到建置好的Json檔案。此檔案包含了合約的ABI定義以及Bytecode

n   我們稍後會需要這些JSON檔案

  • 建置完成後,我們需要修改migration/2_deploy_contracts.js這個檔案。這個檔案中會指定我們要佈署到Ethereum上的合約。將檔案修改如下:

var oracle = artifacts.require("../contracts/OracleContract4.sol");

var resolver = artifacts.require("../contracts/OracleResolver4.sol");

module.exports = function(deployer, network , accounts) {

        deployer.deploy(oracle);

        deployer.deploy(resolver);

};

  • 回到console,執行truffle migrate將合約佈署到Ethereum上;你應該會看到類似以下的畫面表示佈署成功。

n   OracleContract以及OracleResolver的位址記下來,稍後我們會用到。

  • 如果出現Authentication Neded:Password or unlock的錯誤;表示帳號未解鎖,必須解鎖才能進行動作。

n   解鎖可以透過ChromeMetaMask工具來解鎖;將RPC位址設定好後輸入帳號密碼即可解鎖帳號。

  • 接著我們需要先把剛剛記錄下來的OracleContract位址與OracleResolver位址轉換為Checksum Address;我們可以透過以下的Node.JS程式進行轉換。

var tx = 'http://{TXNODE IP}:8545';

var passpharse = '{Password}';

var fs = require('fs');

var Web3 = require('web3');

var web3 = new Web3();

var oracleContractAddress = '0x08c152C90F2775915B1EBC88C218A9B86307A775';//deployed contract checksum address

var clientContractAddress = '0xc3a486e2c58d8b3713af5848eb4303dc7e81946b';

 

web3.setProvider(new web3.providers.HttpProvider(tx));

 

//unlock accounts that we'll be using

web3.personal.unlockAccount(web3.eth.accounts[0],passpharse,30000);  

 

var checksum = web3.toChecksumAddress(oracleContractAddress);

  • 將轉換完成的Checksum address記錄下來。
  • 接著我們需要建立我們實際上要使用的合約;下面黃色的部分即是需要填上剛剛記錄下來的Checksum Address

n   在這個SOL檔案中,我們建立了兩個合約:OracleApp4myClientContract4

n   OracleApp4合約提供了實際上與Oracle溝通的方式;我們透過OracleResolver取得Oracle位址,並提供queryOracle方法給myClientContract4呼叫以與Oracle溝通。

n   myClientContract4合約基本上沒有做什麼事情,只是提供了一些方法供調用測試。

pragma solidity ^0.4.10;

 

import "./OracleContract4.sol";

import "./OracleResolver4.sol";

 

//my client contract

contract OracleApp4 {

    OracleResolver4 public resolver;

    OracleContract4 public oracle;

 

    function OracleApp4() {

        resolver = OracleResolver4(0x444078Aa15FE48B152245744cBD16023B2444C0e);//change the address to your OracleResolver.sol checksum address

        resolver.setOracleAddress(0x08c152C90F2775915B1EBC88C218A9B86307A775);//change the address to your Oracle.sol checksum address

        oracle = OracleContract4(resolver.getOracleAddress());

    }

 

    modifier myOracleAPI {

        _;

    }

 

    modifier onlyFromCallback {

        require(msg.sender == oracle.callbackAddress());

        _;

    }

 

    function queryOracle(string _query) internal myOracleAPI returns(bytes32 id) {

        return oracle.query(_query);

    }

 

    function _callback(bytes32 _id, string result) onlyFromCallback {

        //callback function for offchain to call back

 

    }

}

 

contract myClientContract4 is OracleApp4 {

    address owner;

    string public key;

    string public state;

    bytes32 id;

    event OnCallBack(bytes32 __id, string __result);

     // override

    function _callback(bytes32 _id, string result) onlyFromCallback {

        state = result;

        OnCallBack(_id, result);

    }

    function updateContract(string _state) myOracleAPI {

        state = _state;

        queryOracle(_state);

    }

    function updateContract2(string _state) myOracleAPI {

        state = _state;

    }

    function updateContract3(string _state) myOracleAPI {

        oracle.query(_state);

        state = _state;

    }

    function updateContract4(string _state) myOracleAPI {

        oracle.query(_state);

    }     

    function updateContract5() myOracleAPI returns (uint) {

        return  oracle.getRandomNumber();

    }

}

  • 執行rm build/ -R將上一次truffle建置的結果刪除;請注意,這個動作一定要做,否則稍後會無法佈署我們的新合約。

  • 接著執行truffle compiletruffle migrate並將myClientContract4的部署位址記錄下來。
  • 假如一切正常,那麼接下來我們就可以開始建立Offchain listener了;請注意黃色的部分是Oracle合約的Checksum address

'use strict';

/*

OracleResolver4: 0x444078Aa15FE48B152245744cBD16023B2444C0e

OracleContract4: 0x08c152C90F2775915B1EBC88C218A9B86307A775

OracleApp4: 0x5074bf9892a1ce5be4126e53b8d7addc4b04f918

*/

 

var tx = 'http://{TXNODE IP}:8545';

var passpharse = '{Password}';

var fs = require('fs');

var Web3 = require('web3');

var web3 = new Web3();

var oracleContractAddress = '0x08c152C90F2775915B1EBC88C218A9B86307A775';//deployed contract checksum address

var clientContractAddress = '0xc3a486e2c58d8b3713af5848eb4303dc7e81946b';

 

web3.setProvider(new web3.providers.HttpProvider(tx));

 

//unlock accounts that we'll be using

web3.personal.unlockAccount(web3.eth.accounts[0],passpharse,30000);   //sender

web3.personal.unlockAccount(web3.eth.accounts[1],passpharse,30000);   //oracle

 

function waitForTransactionReceipt(hash) {

    console.log('waiting for contract to be mined');

    const receipt = web3.eth.getTransactionReceipt(hash);

    // If no receipt, try again in 1s

    if (receipt == null) {

        setTimeout(() => {

            waitForTransactionReceipt(hash);

        }, 1000);

    } else {

        // The transaction was mined, we can retrieve the contract address

        console.log('contract address: ' + receipt.contractAddress);

        //var result2 = cf3.UpdateContract.sendTransaction('newState',1,{from:web3.eth.accounts[0]});

        console.log('receipt=' + JSON.stringify(receipt));

    }

}

 

//configure Oracle callback address

var text = fs.readFileSync('./contracts/OracleContract4.json','utf8');

var oracleInterface = JSON.parse(text);

var oracleweb3.eth.contract(oracleInterface.abi).at(oracleContractAddress);

//set callback address, this is the oracle account address, when oracle callback to our contract, the caller address must match address specified here

var hash = oracle.setCallbackAddress.sendTransaction(web3.eth.accounts[1],{from:web3.eth.accounts[1],gas:3000000}); //'0x1f83fa84c03a2f4e50be16947a3991cab6018376'

waitForTransactionReceipt(hash);

 

//convert clientContractAddress to checksum address

//clientContractAddress = web3.toChecksumAddress(clientContractAddress);

 

var queryEvent = oracle.QueryEvent();

console.log('watching for query event');

 

queryEvent.watch(function(err,result){

    if(!err){

        console.log('QueryEvent fired:' + JSON.stringify(result));

 

    }else{

        console.log('error=' + JSON.stringify(err));

    }

});

 

text = fs.readFileSync('./contracts/myClientContract4.json','utf8');

var myContractInterface = JSON.parse(text);

var myContract = web3.eth.contract(myContractInterface.abi).new({from:web3.eth.accounts[0],gas:3000000,data:myContractInterface.unlinked_binary});

 

  • 執行程式後,Offchain listener就會開始監聽QueryEvent
  • 我們可以透過node執行環境試著呼叫myClientContract4::updateContract看看

 

 

About Me