我们以Chainlink提供的TestnetConsumer合约中的一个requestEthereumPrice 方法为例来简单讲一下请求响应的流程。这个函数定义如下:
function requestEthereumPrice(address _oracle, string _jobId)
public
onlyOwner
{
Chainlink.Request memory req = buildChainlinkRequest(stringToBytes32(_jobId), this, this.fulfillEthereumPrice.selector);
req.add(“get”, “https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD”);
req.add(“path”, “USD”);
req.addInt(“times”, 100);
sendChainlinkRequestTo(_oracle, req, ORACLE_PAYMENT);
}
它所实现的功能就是从指定的API(cryptocompare)获取ETH/USD的交易价格。函数传入的参数是指定的oracle地址和jobId。将一些列的请求参数组好后,调用sendChainlinkRequestTo 方法将请求发出。
sendChainlinkRequestTo是定义在Chainlink提供的库中的一个接口方法,定义如下:
/**
* @notice 向指定的oracle地址创建一个请求
* @dev 创建并存储一个请求ID, 增加本地的nonce值, 并使用`transferAndCall` 方法发送LINK,
* 创建到目标oracle合约地址的请求
* 发出 ChainlinkRequested 事件.
* @param _oracle 发送请求至的oracle地址
* @param _req 完成初始化的Chainlink请求
* @param _payment 请求发送的LINK数量
* @return 请求 ID
*/
function sendChainlinkRequestTo(address _oracle, Chainlink.Request memory _req, uint256 _payment)
internal
returns (bytes32 requestId)
{
requestId = keccak256(abi.encodePacked(this, requests));
_req.nonce = requests;
pendingRequests[requestId] = _oracle;
emit ChainlinkRequested(requestId);
require(link.transferAndCall(_oracle, _payment, encodeRequest(_req)), “unable to transferAndCall to oracle”);
requests += 1;
return requestId;
}
其中link.transferAndCall方法即是ERC677定义的token转账方法,与ERC20的transfer方法相比,它多了一个data字段,可以在转账的同时携带数据。这里就将之前打包好的请求数据放在了data字段,跟随转账一起发送到了oracle合约。transferAndCall 方法定义如下:
/**
* @dev 将token和额外数据一起转移给一个合约地址
* @param _to 转移到的目的地址
* @param _value 转移数量
* @param _data 传递给接收合约的额外数据
*/
function transferAndCall(address _to, uint _value, bytes _data)
public
returns (bool success)
{
super.transfer(_to, _value);
Transfer(msg.sender, _to, _value, _data);
if (isContract(_to)) {
contractFallback(_to, _value, _data);
}
return true;
}
其中的Transfer(msg.sender, _to, _value, _data);是发出一个事件日志:
event Transfer(address indexed from, address indexed to, uint value, bytes data);
将这次转账的详细信息(发送方、接收方、金额、数据)记录到日志中。
Oracle合约在收到转账之后,会触发onTokenTransfer方法,该方法会检查转账的有效性,并通过发出OracleRequest事件记录更为详细的数据信息:
event OracleRequest(
bytes32 indexed specId,
address requester,
bytes32 requestId,
uint256 payment,
address callbackAddr,
bytes4 callbackFunctionId,
uint256 cancelExpiration,
uint256 dataVersion,
bytes data
);
这个日志会在oracle合约的日志中找到,如图中下方所示。链下的节点会订阅该主题的日志,在获取到记录的日志信息之后,节点会解析出请求的具体信息,通过网络的API调用,获取到请求的结果。之后通过提交事务的方式,调用Oracle合约中的fulfillOracleRequest方法,将数据提交到链上。fulfillOracleRequest定义如下:
/**
* @notice 由Chainlink节点调用来完成请求
* @dev 提交的参数必须是`oracleRequest`方法所记录的哈希参数
* 将会调用回调地址的回调函数,`require`检查时不会报错,以便节点可以获得报酬
* @param _requestId 请求ID必须与请求者所匹配
* @param _payment 为Oracle发放付款金额 (以wei为单位)
* @param _callbackAddress 完成方法的回调地址
* @param _callbackFunctionId 完成方法的回调函数
* @param _expiration 请求者可以取消之前节点应响应的到期时间
* @param _data 返回给消费者合约的数据
* @return 外部调用成功的状态值
*/
function fulfillOracleRequest(
bytes32 _requestId,
uint256 _payment,
address _callbackAddress,
bytes4 _callbackFunctionId,
uint256 _expiration,
bytes32 _data
)
external
onlyAuthorizedNode
isValidRequest(_requestId)
returns (bool)
{
bytes32 paramsHash = keccak256(
abi.encodePacked(
_payment,
_callbackAddress,
_callbackFunctionId,
_expiration
)
);
require(commitments[_requestId] == paramsHash, “Params do not match request ID”);
withdrawableTokens = withdrawableTokens.add(_payment);
delete commitments[_requestId];
require(gasleft() >= MINIMUM_CONSUMER_GAS_LIMIT, “Must provide consumer enough gas”);
return _callbackAddress.call(_callbackFunctionId, _requestId, _data); // solhint-disable-line avoid-low-level-calls
}
这个方法会在进行一系列的检验之后,会将结果通过之前记录的回调地址与回调函数,返回给消费者合约:
_callbackAddress.call(_callbackFunctionId, _requestId, _data);
这样一次请求就全部完成了。
总结
本文从预言机的概念开始,通过一个简单的获取ETH价格的例子,讲解了请求/响应模式的Chainlink预言机的基本过程,希望对你理解预言机与Chainlink的运行原理有所帮助。