本博客整理于 cryptozombies.io/zh/lesson
查看原教程请参考如上链接,本博客做个人记录!
从零开始写预言机
CATALOGUE
一、什么是预言机?
网上相关的解释很多,我想说说我的理解。
以太坊的本质就是一个由交易触发的状态机,任何在以太坊上面状态的改变都需要在链外发起相关的(Transaction)交易来实现,区块链无法主动获取区块链以外的数据。而预言机就是一个将区块链下的数据传输至区块链之中的中间件。这个中间件的具体实现利用了区块链中的EventLog日志功能,日志记录了相关的要进行传输上链的数据的信息。
全面一些的解释参考 Chainlink预言机基本原理
二、概览
本预言机实现的功能为从币安的API接口中获取到最新的ETH价格!
要实现预言机的基础功能,需要有两个合约,分别为调用者合约(callerContract.sol),和预言机合约(EthPriceOracle.sol),由调用者合约向预言机合约发起请求,预言机合约接受到请求后,记录一个Event,链下的javascript程序监听预言机合约的Event,如果监听到指定事件就想查询相关的数据,并且在调用预言机合约中的相关函数(setLatestEthPrice),然后setLatestEthPrice函数再调用调用者合约中的callerback函数更新ethPrice的值,这样调用者就收到了所需要的链下数据,并且此数据是被记录在了EventLog中。
现在就开始正式进行编写一个预言机啦!
三、链上合约编写
关于预言机,整个功能的实现主要分为用户合约(callerContract.sol)与预言机合约(EthPriceOracleContract.sol)。利用truffle来方便的为合约进行编译部署测试。
1、创建目录与初始化
1.1 创建项目目录
mkdir oralceDemo && cd oracleDemo
npm初始化
npm init -y
安装npm相关包
分别npm install以下依赖
[email protected] // 实测不加版本号安装不下来,找了好久终于找到一个能安装的版本
web3
bn.js
axios
[email protected] // 必须要加版本号,此版本对应的是solidity 0.5.0的版本,下载最新版可能无法编译!
1.2 Geth私有链 或 其他测试链
具体教程可以参考我的另一篇博客
唯一有区别的是启动geth的参数由些许不同,在后面会赘述
本项目没有用测试链进行测试,但大体类似
1.3 truffle初始化
创建目录
mkdir caller && mkdir oracle
初始化
cd caller && truffle init
cd oracle && truffle init
此刻 ,目录结构大体如下所示
.
├── caller
│ ├── contracts
│ ├── migrations
│ ├── test
│ └── truffle-config.js
├── oracle
│ ├── contracts
│ ├── migrations
│ ├── test
│ └── truffle-config.js
└── package.json
* 来源于 https://cryptozombies.io/zh/lesson/14/chapter/1
非常好!到现在已经完成了本项目中最困难的部分啦!
2、调用者合约(CallerContract.sol )
2.0 调用者合约代码
callerContract.sol
pragma solidity ^0.5.0;
import "./EthPriceOracleInterface.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
contract CallerContract is Ownable {
uint256 private ethPrice;
EthPriceOracleInterface private oracleInstance;
address private oracleAddress;
mapping (uint256 => bool) myRequests;
event newOracleAddressEvent(address oracleAddress);
event ReceivedNewRequestIdEvent(uint256 id);
event PriceUpdatedEvent(uint256 ethPrice,uint256 id);
// 2.1 更新oracle地址,并生成实例
function setOracleInstanceAddress(address _oracleInstanceAddress) public onlyOwner {
oracleAddress = _oracleInstanceAddress;
oracleInstance = EthPriceOracleInterface(oracleAddress);
emit newOracleAddressEvent(oracleAddress);
}
// 更新eth价格--函数返回值是id
function updateEthPrice() public {
uint256 id = oracleInstance.getLatestEthPrice();
myRequests[id] = true;
emit ReceivedNewRequestIdEvent(id);
}
// 回调函数,由oracle调用返回数值
function callback(uint256 _ethPrice,uint256 _id) public onlyOracle {
require(myRequests[_id],"This request is not in my pending list.");
ethPrice = _ethPrice;
delete myRequests[_id];
emit PriceUpdatedEvent(_ethPrice, _id);
}
// 确保只有Oracle合约才能调用回调函数修改价格
modifier onlyOracle() {
require(msg.sender == oracleAddress,"You are not authorized to call this function.");
_;
}
}
EthPriceOracleInterface.sol
预言机合约的接口,指明调用函数的名字,参数列表与返回值
pragma solidity ^0.5.0;
contract EthPriceOracleInterface {
function getLatestEthPrice() public returns (uint256); //返回的是Id
}
除了看代码之外,我对写代码中遇到的一些问题进行了一些整理。
2.1 solidity中一个合约调用另外一个合约该如何实现?
- 编写要调用的合约的函数的接口,如2.0中的
EthPriceOracleInterface.sol
- 在原合约中import 该接口
- 在合约中声明该接口的实例并用该合约地址初始化
- 最后即可在本合约中调用被调用合约的函数
具体过程可以参考上面实例
2.2 函数修饰符如何实现?
由modifier声明
重点关注"_
",该符号代表被修饰的函数的代码所处的位置
modifier onlyOracle() {
require(msg.sender == oracleAddress,"You are not authorized to call this function.");
_;
}
2.3 什么是openzeppelin库?
什么是openzeppelin库 链接
openzeppelin库如何选择 链接
2.4 什么是mapping?
mapping 是solidity中的映射关系,非常经典的应用是在ERC20中用来保存每一个账户的地址的该代币的余额。
2.5 solidity中合约调用合约的时候,msg.sender是谁?
是从外部发起这个交易的人呢?还是这个调用这个合约的合约呢?
分为不同的情况!
参考文章call delegatecall
3、预言机合约(EthPriceContract.sol)
3.0 预言机合约代码
EthPriceOracle.sol
pragma solidity ^0.5.0;
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "./CallerContractInterface.sol";
contract EthPriceOracle is Ownable{
uint private randNonce = 0;
uint private modulus = 1000;
mapping (uint256 => bool) pendingRequests;
event GetLatestEthPriceEvent(address callerAddress,uint id);
event SetLatestEthPriceEvent(uint256 ethPrice,address callerAddress);
// 该函数供caller调用,实现发起一个数据请求!
function getLatestEthPrice() public returns(uint256) {
randNonce++;
uint id = uint(keccak256(abi.encodePacked(now,msg.sender,randNonce))) % modulus;
pendingRequests[id] = true;
emit GetLatestEthPriceEvent(msg.sender, id);
return id;
}
// 调用该函数将ethPrice写入callerContract合约中,调用回调函数
function setLatestEthPrice(uint256 _ethPrice,address _callerAddress,uint256 _id) public onlyOwner {
require(pendingRequests[_id],"This request is not in my pending list.");
delete pendingRequests[_id];
CallerContractInterface callerContractInstance;
callerContractInstance = CallerContractInterface(_callerAddress);
callerContractInstance.callback(_ethPrice, _id);
emit SetLatestEthPriceEvent(_ethPrice, _callerAddress);
}
}
CallerContractInterface.sol
pragma solidity ^0.5.0;
contract CallerContractInterface {
function callback(uint256 _ethPrice,uint256 id) public;
}
3.1 如何在solidity中生成一个不那么安全的随机数?
uint id = uint(keccak256(abi.encodePacked(now,msg.sender,randNonce))) % modulus;
这一行代码就可以啦!
为什么不安全?这个内容在本博客开头的链接中有!
四、总结
到此为止,我们的预言机合约与调用者合约的链上部分就完成啦!
接下来,我们将链下对他们所触发的事件进行处理!