智能合約中使用更安全的隨機數(代碼實戰篇)
Chainlink最近推出一款革命性的產品,VRF—Verifiable Random Function可驗證隨機數,給智能合約帶來了真正安全的隨機數。本文我們就來介紹一下如何在智能合約中使用VRF吧。
我們先簡要介紹一下Chainlink VFR的工作流程。
- 首先,智能合約應用,也就是我們的Dapp,需要先發起一個獲取隨機數的請求,這個請求需要給定一個合約地址,這個合約稱為VRFCoordinator合約。
- 與VRFCoordinator合約所關聯的Chainlink鏈下節點,會(通過橢圓曲線數字簽名算法)生成一個隨機數,以及一個證明。
- Chainlink節點將上面生成的隨機數和證明發送到VRFCoordinator合約中。
- VRFCoordinator合約收到隨機數和證明後,會對通過證明來驗證所生成隨機數的合法性。
- 隨機數驗證成功後,會將隨機數發送回用户的智能合約應用
整個過程中有兩次的交易提交確認的過程,用户合約需要支付LINK給VRF合約作為交易費用。
下面我們就通過寫一個猜數字的小遊戲,來學習如何使用Chainlink VRF。
首先,新建一個truffle項目,安裝Chainlink開發包
mkdir vrf; cd vrf
truffle init
npm install @chainlink/contracts --save
在contracts目錄下新建一個合約文件MyVrfContract.sol。引入vrf的庫文件:
pragma solidity 0.6.2;
import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol";
VRFConsumerBase是我們需要繼承的基類,使用Chainlink VRF的很多方法都在這個合約裏定義了,因此我們來簡單介紹一下這個合約。
abstract contract VRFConsumerBase is VRFRequestIDBase {
...
function fulfillRandomness(bytes32 requestId, uint256 randomness)
external virtual;
function requestRandomness(bytes32 _keyHash, uint256 _fee, uint256 _seed)
public returns (bytes32 requestId)
{
...
}
...
}
上面列出了VRFConsumerBase合約的兩個基本方法,一個是requestRandomness方法,它是用來發起一個VRF請求的方法,在調用的時候呢,需要傳入三個參數:
_keyHash: 節點的公鑰哈希。因為隨機數及其證明的生成,是通過橢圓曲線密碼學來完成的,所以需要一對公鑰和私鑰。公鑰是節點公開的密鑰,目前可用的VRF節點公鑰及VRF節點的其他相關信息,可用在Chainlink官方文檔上查到。_fee: 用户發起一次VRF請求所需要花費的費用,這個費用也可以在節點公佈的相關信息中查閲到。如果費用沒有達到節點的最低要求,那麼VRF請求無法完成。費用是以LINK token形式支付,所以用户合約需要持有足夠的LINK token。_seed: 用户提供的種子,用於生成隨機數。用户需要給出高質量的種子。這裏我們需要解釋一下VRF的特點,VRF是通過種子與節點私鑰做橢圓曲線中的計算得來的,同一個種子對應的隨機數也是相同的,所以用户需要每次都給出不一樣的且不可預測的種子。這很重要。因為任何可以左右用户種子的因素,都可以與鏈下的節點勾結作惡,生成他們想要的隨機數,從而損害用户的利益。區塊鏈中,我們很容易就找到這麼一個隨機源就是區塊哈希,但是區塊哈希是可以被礦工控制的(雖然很難),所以建議不能僅使用區塊鏈哈希,還需要與其他隨機源一起使用生成種子。比如下面就是一個例子。
function makeRequest(uint256 userProvidedSeed)
public returns (bytes32 requestId)
{
uint256 seed = uint256(keccak256(abi.encode(userProvidedSeed, blockhash(block.number)))); // Hash user seed and block hash
return requestRandomness(keyHash, fee, seed);
}
VRFConsumerBase合約中的另外一個重要的方法是fulfillRandomness,這是一個虛函數,需要在繼承的類中加以實現。這個函數的主要作用就是接受節點生成的隨機數。用户合約在複寫這個函數的時候就可以添加一些自己的業務邏輯代碼。
另外,VRFConsumerBase合約中海油一個構造函數,需要調用者提供_vrfCoordinator合約的地址和所在網絡中LINK token的地址。
constructor(address _vrfCoordinator, address _link) public {
vrfCoordinator = _vrfCoordinator;
LINK = LinkTokenInterface(_link);
}
在我們的用户合約中,需要通過構造函數,來進行一些初始化的工作。初始化的內容包括向基類,也就是VRFConsumerBase類的構造函數進行賦值初始化,還包括初始化本合約所用到的一些常量參數。
constructor(address _vrfCoordinator, address _link)
VRFConsumerBase(_vrfCoordinator, _link) public {
// 其他一些初始化參數
}
下面就需要發起VRF請求來獲取隨機數,調用一下基類裏的requestRandomness方法就可以。
bytes32 internal constant keyHash = 0xced103054e349b8dfb51352f0f8fa9b5d20dde3d06f9f43cb2b85bc64b238205;
uint256 internal constant fee = 10 ** 18;
function getRandomness(uint256 userProvidedSeed)
public returns (bytes32 requestId) {
require(LINK.balanceOf(address(this)) > fee, "Not enough LINK - fill contract with faucet");
uint256 seed = uint256(keccak256(abi.encode(userProvidedSeed, blockhash(block.number)))); // Hash user seed and blockhash
bytes32 _requestId = requestRandomness(keyHash, fee, seed);
return _requestId;
}
在這個請求函數中,我們把用户合約和區塊哈希共同作為種子。keyHash和fee就作為常量直接寫在合約裏,也可以寫到setter方法中或者構造函數中。
接下來就是複寫接收方法。
uint256 public random;
function fulfillRandomness(bytes32 requestId, uint256 randomness)
external override {
random = randomness.mod(100).add(1);
}
這裏我們就直接賦值給我們的一個公開變量,方便我們查看結果。節點生成的隨機數是一個非常大的一個數,我們需要對原始的隨機數做一個模運算,以方面我們後面的處理。加一是為了避免得到0值。
下面我們貼一下完整的代碼:
pragma solidity 0.6.2;
import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol";
// import this if you are using remix
// import "https://raw.githubusercontent.com/smartcontractkit/chainlink/develop/evm-contracts/src/v0.6/VRFConsumerBase.sol";
contract MyVRFContract is VRFConsumerBase {
constructor(address _vrfCoordinator, address _link)
VRFConsumerBase(_vrfCoordinator, _link) public {
}
bytes32 internal constant keyHash = 0xced103054e349b8dfb51352f0f8fa9b5d20dde3d06f9f43cb2b85bc64b238205;
uint256 internal constant fee = 10 ** 18;
function getRandomness(uint256 userProvidedSeed) public returns (bytes32 requestId) {
require(LINK.balanceOf(address(this)) > fee, "Not enough LINK - fill contract with faucet");
uint256 seed = uint256(keccak256(abi.encode(userProvidedSeed, blockhash(block.number))));
bytes32 _requestId = requestRandomness(keyHash, fee, seed);
return _requestId;
}
uint256 public random;
function fulfillRandomness(bytes32 requestId, uint256 randomness) external override {
random = randomness.mod(100).add(1);
}
}
好了,這樣一個簡單的用户合約寫完了,我們就可以寫根據之前的方法測試一下啦。由於步驟都是類似的,這裏就不在贅述了。可以參考這裏進行測試。
總結,雖然VRF內涵了非常複雜深奧的密碼學知識,但是得益於Chainlink工程師們的良好設計,已經把複雜性極大程度的掩蓋了,作為Dapp開發者,我們在使用起來非常的容易上手,只需要注意提供好種子就可以了。快來嘗試一下吧!
參考文檔:
https://blog.chain.link/verifiable-random-functions-vrf-random-number-generation-rng-feature/
https://docs.chain.link/docs/chainlink-vrf
https://docs.chain.link/docs/vrf-contracts
https://learnblockchain.cn/article/1056
https://www.trufflesuite.com/docs/truffle/quickstart