iT邦幫忙

2022 iThome 鐵人賽

DAY 15
2
Web 3

從以太坊白皮書理解 web 3 概念系列 第 16

從以太坊白皮書理解 web 3 概念 - Day15

  • 分享至 

  • xImage
  •  

從以太坊白皮書理解 web 3 概念 - Day15

Learn Solidity - Day 7 - Data Feeds and Computation

今天將會透過 lession 19- Data Feeds and Computation 來介紹如何引用 Oracle

Ethereum Oracle 是讓 Smart Contract 讀取外部資料的一種方式,因為預設 Smart Contract 寫入的邏輯都是透過 Blockchain 交易與 EVM 來運行所以都是具有可預測性,所以需要這樣的機制來增加應用彈性。

當 Smart Contract 有使用 Oracle 來引入外部資料,這類 Smart Contract 稱作 Hybrid Smart Contract

Chainlink Data 引入介紹

因為 Smart Contract 本身無法直接存取外部資料

所以必須透過 Oracle 這個機制引入外部資料提供者

然而如果使用中心化的 Oracle 會導致資料會容易被單點錯誤所影響

所以理想上的作法是使用去中心化 Oracle 網路(Decentrallized Oracle Network) 以及去中心化資料來。

ChainLink 是一個去中心化 Oracle 網路(Decentrallized Oracle Network)的框架,可以透過多個資料來源拿取資料。
去中心化 Oracle 網路(Decentrallized Oracle Network)以去中心化方式匯集資料把資料放到區塊鏈上的一個 Smart Contract(稱作 price reference feed 或是 data feed) 讓其他人使用。

所以要使用 ChainLink ,就是讀取使用 ChainLink 網路資料的 SmartContract。

實作

  1. 建立一個 Contract 名稱叫作 PriceConsumerV3
pragma solidity ^0.6.7; //1. Enter Solidity version here

//2. Create the `PriceConsumerV3`contract
contract PriceConsumerV3 {
    
}

從 npm 以及 github 引入 ChainLink

當要使用其他寫好的 code

solidity 可以直接使用 import 從可以讀取到的資源引入

這邊需要使用 ChainLink 的 AggregatorV3Interface

以下是 interface 的內容

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

interface AggregatorV3Interface {

  function decimals()
    external
    view
    returns (
      uint8
    );

  function description()
    external
    view
    returns (
      string memory
    );

  function version()
    external
    view
    returns (
      uint256
    );

  // getRoundData and latestRoundData should both raise "No data present"
  // if they do not have data to report, instead of returning unset values
  // which could be misinterpreted as actual reported values.
  function getRoundData(
    uint80 _roundId
  )
    external
    view
    returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    );

  function latestRoundData()
    external
    view
    returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    );

}

實作

  1. import "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol"
pragma solidity ^0.6.7;

// Start here
import "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol";
contract PriceConsumerV3 {

}

實作 AggregatorV3Interface

上面引入 interface

接下就可以透過內部的 Contract 來讀取想要的資料

實作

  1. 建立變數 AggregatorV3Interface priceFeed public;
  2. 建立一個 constructor
  3. 初始化 AggregatorV3Interface 使用參數 0x8A753747A1Fa494EC906cE90E9f37563A8AF630e ,並且把結果存在 priceFeed
pragma solidity ^0.6.7;

import "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol";

contract PriceConsumerV3 {

  // 1. Create a `public` variable named `priceFeed` of type `AggregatorV3Interface`.
  AggregatorV3Interface public priceFeed;
  // 2. Create a constructor
  constructor() public {
    priceFeed = AggregatorV3Interface(0x8A753747A1Fa494EC906cE90E9f37563A8AF630e);
  }
  // 3. Instantiate the `AggregatorV3Interface` contract
  

}

使用 Tuples

現在需要使用 lastestRoundData 來取得最新的資料

其中會有以下資訊:

  • roundId: 代表每個不同的 round
  • answer: 目前的 price
  • startedAt: round 開始的 timestamp
  • updatedAt: round 更新的 timestamp
  • answeredInRound: price 被計算出來的 roundId

Tuples

在呼叫 function 之前,需要知道資料 tuples 結構

在 solidity, tuple 是一種把多個變數組合成一個結構的表達式

如下

function latestRoundData()
    external
    view
    returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    );

這個 latestRoundData 的回傳值就是一個 tuple

接收 tuple 回傳值語法如下

(uint80 roundId, int answer, uint startedAt, uint updatedAt, uint80 answeredInRound) = priceFeed.latestRoundData();

另外也可以把回傳值變數重新命名如下

(uint80 roundId, int price, uint startedAt, uint updatedAt, uint80 answeredInRound) = priceFeed.latestRoundData();

此外,也可以只接收部份回傳值如下

 (,int price,,,) = priceFeed.latestRoundData();

實作接收回傳值

  1. 建立 function getLatestPrice() public view returns(int)
  2. 呼叫 priceFeed Contract 的 lastRoundData 方法
    並且只存下 price 在一個 int 變數
  3. 回傳 price
pragma solidity ^0.6.7;

import "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol";

contract PriceConsumerV3 {
  AggregatorV3Interface public priceFeed;

  constructor() public {
    priceFeed = AggregatorV3Interface(0x8A753747A1Fa494EC906cE90E9f37563A8AF630e);
  }

  // Start here
  function getLatestPrice() public view returns(int) {
    (,int price,,,) = priceFeed.latestRoundData();
    return price;
  }
}

處理 Chianlink Data Feeds Decimals

如果直接讀取 getLatestPrice 得到的輸出格式會什麼?

會發現他的回傳值格式如下

310523971888

但實際的價格卻是 $3,105.52.

所以必須要呼叫一個叫作 decimal 的 function

來取出換成 decimal 的值

實作 getDecimals

  1. 建立一個 function getDecimals() public view returns(uint8)
  2. 呼叫 priceFeed.decimals() ,把結果存在 uint8 decimals
  3. 回傳 decimals
pragma solidity ^0.6.7;

import "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol";

contract PriceConsumerV3 {
  AggregatorV3Interface public priceFeed;

  constructor() public {
    priceFeed = AggregatorV3Interface(0x8A753747A1Fa494EC906cE90E9f37563A8AF630e);
  }

  function getLatestPrice() public view returns (int) {
    (,int price,,,) = priceFeed.latestRoundData();
    return price;
  }

  // Start here
  function getDecimals() public view returns(uint8) {
    uint8 decimals = priceFeed.decimals();
    return decimals;
  }
}

Chainlink VRF 簡介

在前面 ZombieContract 有使用一個隨機數的功能

然而這個隨機數功能並非是完全無法預測也就是並非真的隨機

前面實作的方式是透過 kecca256 產生的 hash

並且使用時間參數 block.timestamp 與一個亂數種子 block.difficulty 以及簽章者的 address msg.sender

具體實作如下

uint(keccak256(abi.encodePacked(msg.sender, block.difficulty, block.timestamp)));
  • msg.sender: 這個資訊做簽章者會知道
  • block.difficulty: 這個資訊挖礦節點會知道
  • block.timestamp: 可以從挖礦速度推斷出來

所以當這3個資訊一出其實這個隨機數功能是一個可以預測的數列

因此要做到真正的隨機數必須透過 Chainlink VRF(Verifiable Randomness Function) 來處理

Chainlink VRF(Verifiable Randomness Function)

Chainlink VRF(Verifiable Randomness Function) 是一種從鏈下取隨機數的一種方法,使用的是被證明過的密碼學方法。

這部份是為了達到真正的隨機性。

其他想從鏈下取得隨機性的方法是透過呼叫鏈下的 api 來做到,但如果鏈下 api 對應的 service 掛了或是被竄改,就可能拿到非隨機性的結果。

VRF 使用鏈上的驗證 Contract 來使用密碼學上方式證明有達到真正的隨機性。

流程如下:

基礎 Request 模型

流程分成以下步驟

  1. 被呼叫的 Contract 傳送一個 Request 在 transaction 內
  2. 然後被呼叫的 Contract 或是 Oracle Contract 發起一個 event
  3. Chainlink 節點(鏈下)會監聽這個 event
    event 裡有包含的詳細的 Request 細節
  4. Chainlink 節點會根據 event 內容做運算並且發起一個 transaction 來把運算的結果送到驗證的 Contract 最後呼叫 Callee Contract 的 function
  5. 透過 VRF Contract 驗證來保正這樣的情況下可以達到真正的隨機性

可以發現為了要使用 Oracle 除了本身運算之外,還需要多付一個關於 Oracle 驗證的 Transaction fee 。而這個 Transaction fee 是透過 Chainlink token 支付的。

為何不直接使用 data feeds

看上面的流程與 data feed 與類似

那位何不直接使用 data feed 要透過 VRF。

主要是節省成本,因為是用 data feed 代表所有 Chianlink 節點都需要做這個運算,而不是像 VRF 只需要單個節點。

實作使用 VRF 的邏輯

  1. 引用 VRFConsumerBase.sol 到要實作的 contract 內
pragma solidity ^0.6.6;

// 1. Import the ""@chainlink/contracts/src/v0.6/VRFConsumerBase.sol" contract
import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol";
contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }

    function _generatePseudoRandomDna(string memory _str) private view returns (uint) {
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
    }

}

實作與 Chainlink Node 互動

為了要與 Chainlink Node 互動

需要知道一些變數

  • Chainlink token Contract 的位址。這個個用來判斷是否有足夠的 Chainlink token
  • VRF coordinator Contract 的位址。需要用來做驗證的 Contract address
  • Chainlink Node 的 keyhash 。這個是用來指定要使用哪一個 Chainlink Node。
  • Chainlink Node fee。這個代表 Chainlink 所需要收的費用。會以 Chainlink token 作為單位。

Constructor 繼承

import "./Y.sol";
contract X is Y {
    constructor() Y() public{
    }
}

上面就是使用繼承來實作 constructor

而要繼承 VRFConsumerbase 的 constructor 語法如下:

constructor() VRFConsumerBase(
    0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator
    0x01BE23585060835E02B77ef475b0Cc51aA1e0709  // LINK Token
) public{

}

實作

  1. 讓 ZombieFactory 繼承 VRFConsumerbase Contract
  2. 實作 ZombieFactory constructor 繼承 VRFConsumerBase 的 constructor
pragma solidity ^0.6.6;
import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol";

// 1. Have our `ZombieFactory` contract inherit from the `VRFConsumerbase` contract
contract ZombieFactory is VRFConsumerbase {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    // 2. Create a constructor
	constructor() VRFConsumerBase(
        0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator
        0x01BE23585060835E02B77ef475b0Cc51aA1e0709  // LINK Token
    ) public{

    }
    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }

    function _generatePseudoRandomDna(string memory _str) private view returns (uint) {
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
    }

}

建立 Chainlink VRF 變數

  1. 建立以下變數:
    bytes32 public keyHash;
    uint256 public fee;
    uint256 public randomResult;
  2. 在 construtor 內做以下更新:
    keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311;
    fee = 100000000000000000;
pragma solidity ^0.6.6;
import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol";

contract ZombieFactory is VRFConsumerbase {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    // 1. Define the `keyHash`, `fee`, and `randomResult` variables. Don't forget to make them `public`.
    bytes32 public keyHash;
    uint256 public fee;
    uint256 public randomResult;
    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    constructor() VRFConsumerBase(
        0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator
        0x01BE23585060835E02B77ef475b0Cc51aA1e0709  // LINK Token
    ) public{
      // 2. Fill in the body
      keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311;
      fee = 100000000000000000;
    }

    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }

    function _generatePseudoRandomDna(string memory _str) private view returns (uint) {
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
    }

}

實作 requestRandomness 與 fulfillRandomness 功能

在 Chainlink VRF 流程

需要定義以下功能:

  1. 一個用來發 request 要求 random number 的 function
  2. 一個用來從 Chainlink Node 接收 random number 的 function

因為已經引入了 VRFConsumerbase Contract

所以可以使用以下兩個內建的 function

  1. requestRandomness: 與 Chainlink Node 溝通並且需要會傳送 token 給 Chainlink Node ,會 assign requestId 給發送的 request
  2. fulfillRandomness: 接收 Chainlink Node 的結果並且與 呼叫 VRF Coordinator,最後把 requestId 與結果回傳

實作步驟

  1. 建立 function getRandomNumber() public returns(bytes32 requestId)
    function 會回傳 requestRandomness(keyHash, fee) 的結果。
  2. 建立 function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override
    function 會做以下更新
    randomResult = randomness;

備註 這邊特別限定 fulfillRandomness 為 internal override 只讓 VRF Coordinator Contract 做呼叫

pragma solidity ^0.6.6;
import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol";

contract ZombieFactory is VRFConsumerbase {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    bytes32 public keyHash;
    uint256 public fee;
    uint256 public randomResult;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    constructor() VRFConsumerBase(
        0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator
        0x01BE23585060835E02B77ef475b0Cc51aA1e0709  // LINK Token
    ) public{
        keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311;
        fee = 100000000000000000;

    }

    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }

    // 1. Create the `getRandomNumber` function
    function getRandomNumber() public returns (bytes32 requestId) {
        return requestRandomness(keyHash, fee);
    }
    // 2. Create the `fulfillRandomness` function
    function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
        randomResult = randomness;
    }
    function _generatePseudoRandomDna(string memory _str) private view returns (uint) {
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
    }

}

把之前 舊的 RandomNumber 的 function 移除

pragma solidity ^0.6.6;
import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol";

contract ZombieFactory is VRFConsumerbase {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    bytes32 public keyHash;
    uint256 public fee;
    uint256 public randomResult;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    constructor() VRFConsumerBase(
        0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator
        0x01BE23585060835E02B77ef475b0Cc51aA1e0709  // LINK Token
    ) public{
        keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311;
        fee = 100000000000000000;

    }

    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }


    function getRandomNumber() public returns (bytes32 requestId) {
        return requestRandomness(keyHash, fee);
    }

    function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
        randomResult = randomness;
    }

    
}

到此,就透過 Chainlink VRF 完成 random number功能。


上一篇
從以太坊白皮書理解 web 3 概念 - Day14
下一篇
從以太坊白皮書理解 web 3 概念 - Day16
系列文
從以太坊白皮書理解 web 3 概念32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言