Chainlink VRF


개인 스터디 차원에서 Chainlink VRF - Overview를 번역하고 이해를 돕고자 내용을 보충하여 정리하였습니다.

Chainlink VRF는 스마트 컨트랙트가 안전하고 사용하기 쉽게 랜덤한 값을 가져올 수 있게 해주는 도구입니다.

Generate Random Numbers for Smart Contracts using Chainlink VRF | Chainlink Documentation

랜덤한 값을 요청하는 두 가지 방법

체인링크 VRF v2는 랜덤한 값을 요청하는 두 가지 방법을 제공합니다.

  • Subscription
    • Subscription 계정을 생성하고 LINK Token으로 잔액을 충전합니다.
      • 사용자는 여러 개의 Consumer 컨트랙트를 Subscription 계정에 연결할 수 있습니다.
    • Consumer 컨트랙트가 랜덤한 값을 요청하면 랜덤 요청이 완료된 후 거래 비용이 계산되며 그에 따라 Subscription 계정에 있는 LINK Token 잔액이 차감됩니다.
    • 이 방법을 사용하면 단일 Subscription에서 여러 Consumer 컨트랙트에 대한 요청에 LINK Token 잔액을 지원할 수 있습니다.
  • Direct funding
    • Consuming 컨트랙트는 랜덤한 값을 요청할 때 LINK로 직접 지불할 수 있습니다.
    • 그러기 위해선 사전에 consumer 컨트랙트가 직접 LINK Token을 가지고 있어야하며 랜덤한 값 요청에 대한 비용을 지불할 수 있는 충분한 LINK Token이 있는지 확인해야합니다.

올바른 방법 선택하기

앞서 두가지 방법 중 어떤 방법이 더 나을지는 사용자가 선택해야합니다.

Generate Random Numbers for Smart Contracts using Chainlink VRF | Chainlink Documentation

지원하는 Networks

Chainlink VRF v2 Supported Networks | Chainlink Documentation

Chainlink VRF Contract Addresses | Chainlink Documentation

Getting Started


이 가이드에서는 블록체인에서 랜덤한 값을 생성하는 방법에 대해 알아볼 것입니다. 여기에는 Chainlink Oracle로 Request와 Receive Cycle를 구현하는 방법과 사용자가 만든 스마트 컨트랙트에서 Chainlink VRF로 랜덤을 생성하는 방법을 배우게됩니다.

블록체인에서 랜덤한 값을 생성하기란 매우 어렵습니다. 블록체인의 모든 노드는 동일한 결론에 도달하고 합의를 형성해야 하기 때문입니다. 난수는 다양한 블록체인 애플리케이션에서 유용하지만, 스마트 컨트랙트에서는 기본적으로 생성할 수 없습니다. 이 문제에 대한 해결책은 Chainlink의 검증 가능한 난수 함수 라고도 알려진 Chainlink VRF입니다.

Request 및 Receive cycle란 뭘까요?

Chainlink의 “Request and Receive” cycle은 블록체인에서 무작위성을 안전하게 활용하기 위한 과정입니다. 기본적으로, 블록체인 자체에서 직접적인 랜덤한 값을 생성하는 것은 안전하지 않습니다. 왜냐하면 그 결과가 블록체인에 저장되면, 어떤 참여자든 그 값을 가져와 결과를 예측할 수 있기 때문입니다.

따라서 Chainlink는 이 문제를 해결하기 위해 “Request and Receive” 사이클을 사용합니다. 이 사이클의 주요 단계는 다음과 같습니다.

  1. 요청(Request): 스마트 컨트랙트가 Oracle에게 무작위 숫자를 요청합니다.
  2. Oracle 처리: Oracle은 무작위 숫자와 그 숫자가 어떻게 생성되었는지 증명하는 암호화 증거를 생성합니다.
  3. 반환(Receive): Oracle은 생성된 무작위 숫자와 암호화된 증거를 요청한 스마트 컨트랙트에 반환합니다.

이 과정을 통해, 스마트 컨트랙트는 안전하게 무작위 값을 받아올 수 있습니다. 이 값은 예측이나 조작이 불가능하며, Chainlink Oracle에 의해 안전하게 생성되었음을 암호화 증거를 통해 검증할 수 있습니다.

난수 생성을 위한 결제 절차는 어떻게 될까요?

VRF에서 요청(Request)를 하기 위해서는 앞서 말씀드린것과 같이 LINK Token이 필요합니다. LINK Token은 Subscription 계정을 통해 결제를 받습니다. Subscription 관리자를 사용하면 계정을 만들고 VRF 요청에 대한 자금을 선결제할 수 있으므로 모든 애플리케이션 요청의 자금이 한 곳에서 관리됩니다.

설명을 위해 Chainlink VRF를 사용해 랜덤한 값을 생성해내는 애플리케이션을 만들겠습니다.

이 애플리케이션에 사용되는 컨트랙트에는 왕좌의 게임 테마가 있습니다.

컨트랙트가 Chainlink VRF에 무작위한 값을 요청하면, 무작위성 결과는 20면 주사위를 굴리는 것처럼 1에서 20사이의 숫자로 변환됩니다. 각 숫자는 왕좌의 게임 하우스를 나타냅니다. 주사위가 1에 나오면 사용자는 타르가르옌 가문, 2에 나오면 라니스터 가문 등의 가문에 배정됩니다.

주사위를 굴릴 때 주소 변수를 사용하여 각 House에 어떤 주소가 할당되어 있는지를 추적합니다.

컨트랙트에는 다음과 같은 기능이 있습니다.

  • rollDice: Chainlink VRF에 랜덤한 값을 요청합니다.
  • fulfillRandomWords: 오라클이 결과를 다시 전송하는데 사용하는 함수입니다.
  • house: 주소의 할당된 house를 확인합니다.

Chainlink VRF Request는 Subscription 계정에서 자금을 지원받습니다.

VRFConsumerBaseV2 및 VRFCoordinatorV2 인터페이스 임포트하기

Chinlink는 Oracle에서 데이터를 더 쉽게 사용할 수 있도록 컨트랙트 라이브러리를 관리합니다. 체인링크 VRF의 경우 아래와 같은 라이브러리를 사용하게 됩니다.

  • VRFConsumerBaseV2: 사용자가 생성한 컨트랙트에서 Import 및 상속해야하는 컨트랙트입니다.
  • VRFCoordinatorV2Interface: VRF Coordinator와 통신하기 위해 Import 해야하는 인터페이스입니다.
  // SPDX-License-Identifier: MIT
  pragma solidity ^0.8.7;

  import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
  import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";

  contract VRFD20 is VRFConsumerBaseV2 {

  }

Contract Variables

이 예제는 Sepolia 테스트넷에 맞게 조정되었지만 구성을 변경하여 지원되는 모든 네트워크에서 실행할 수 있습니다.

  uint64 s_subscriptionId;
  address s_owner;
  VRFCoordinatorV2Interface COORDINATOR;
  address vrfCoordinator = 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625;
  bytes32 s_keyHash = 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;
  uint32 callbackGasLimit = 40000;
  uint16 requestConfirmations = 3;
  uint32 numWords =  1;
  • uint64 s_subscriptionId: 이 컨트랙트가 Request 시 LINK Token을 위해 사용하는 구독 ID입니다. 생성자에서 초기화됩니다.
  • address s_owner: 배포할 컨트랙트의 소유자 주소입니다. 이 주소는 생성자에서 초기화되며, 컨트랙트를 배포할 때 사용하는 주소가 됩니다.
  • VRFCoordinatorV2Interface COORDINATOR: 이 컨트랙트가 사용할 Chainlink Coordinator Interface 컨트랙트의 주소입니다. 생성자에서 초기화됩니다.
  • address vrfCoordinator: VRF Coordinator 컨트랙트의 주소입니다.
  • bytes32 s_keyHash: Gas lane keyHash 값으로, 요청에 대해 지불할 수 있는 최대 Gas Price입니다. Request에 대한 응답으로 실행되는 오프체인 VRF 작업의 ID 역할을 합니다.
  • uint32 callbackGasLimit: 랜덤한 값이 생성된 후, 사용자가 만든 컨트랙트로 해당 랜덤값을 전달받는데 필요한 Gas Price limit입니다.
    • fulfillRandomWords() 함수에 대한 콜백 요청에 사용할 Gas limit입니다. 이 값은 Coordinator 컨트랙트의 max gas limit 보다 작아야합니다. 더 큰 요청의 경우 이 값을 조정해야합니다. “callbackGasLimit”이 충분하지 않으면 콜백이 실패하고 요청된 랜덤한 값을 생성하기 위해 수행한 작업에 대한 요금이 Subscription에 청구됩니다.
  • uint16 requestConfirmations: Chainlink 노드가 응답하기 전에 대기해야하는 횟수 입니다. 노드개 대기하는 시간이 길수록 무작위 값을 보안이 강화됩니다. Coordinator 컨트랙트의 minimumRequestBlockConfirmations 보다 커야합니다.
  • uint32 numWords: 요청할 무작위 값의 개수입니다. 단일 콜백으로 여러 개의 무작위 값을 사용할 수 있다면 무작위 값당 소비하는 가스 양을 줄일 수 있습니다.
mapping(uint256 => address) private s_rollers;
mapping(uint256 => uint256) private s_results;
  • s_rollers는 요청이 이루어질 때 반환되는 requestID와 roller의 주소 사이의 매핑을 저장합니다. 이는 컨트랙트 결과가 돌아올 때 누구에게 할당할지 추적할 수 있도록 하기 위함입니다.
  • s_results는 roller와 주사위의 굴림 결과를 저장하기 위해 사용합니다.

Initializing the contract

Coordinator와 Subscription ID는 컨트랙트의 생성자에서 초기화해야합니다. VRFConsumerBaseV2를 올바르게 사용하려면 생성자에 VRF coordinator 주소도 전달해야합니다. 스마트 컨트랙트를 생성하는 주소는 컨트랙트의 소유자이며, modifier인 onlyOwner()는 소유자만 일부 작업을 수행할 수 있는지 확인합니다.

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

  import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
  import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";

  contract VRFD20 is VRFConsumerBaseV2 {
      // variables
      // ...

      // constructor
      constructor(uint64 subscriptionId) VRFConsumerBaseV2(vrfCoordinator) {
          COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
          s_owner = msg.sender;
          s_subscriptionId = subscriptionId;
      }

      //...
      modifier onlyOwner() {
          require(msg.sender == s_owner);
          _;
      }
  }

rollDice function

rollDice 함수는 다음 작업을 진행합니다.

  1. 각 roller는 하나의 house에만 할당할 수 있으므로 roller가 이미 굴러갔는지 확인합니다.
  2. VRF Coordinator를 호출하여 랜덤한 값을 요청합니다.
  3. requestId와 roller 주소를 저장합니다.
  4. 주사위가 굴러가고 있음을 알리는 Event가 발생합니다.

주사위가 굴려졌지만 결과가 아직 반환되지 않았음을 알리기 위해 ROLL_IN_PROGRESS 상수를 추가해야 합니다. 또한 컨트랙트에 DiceRolled Event를 추가하세요.

참고로 컨트랙트 소유자만 rollDice 함수를 호출할 수 있습니다.

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

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";

contract VRFD20 is VRFConsumerBaseV2 {
    // variables
    uint256 private constant ROLL_IN_PROGRESS = 42;
    // ...

    // events
    event DiceRolled(uint256 indexed requestId, address indexed roller);
    // ...

    // ...
    // { constructor }
    // ...

    // rollDice function
    function rollDice(address roller) public onlyOwner returns (uint256 requestId) {
        require(s_results[roller] == 0, "Already rolled");
        // Will revert if subscription is not set and funded.
        requestId = COORDINATOR.requestRandomWords(
        s_keyHash,
        s_subscriptionId,
        requestConfirmations,
        callbackGasLimit,
        numWords
       );

        s_rollers[requestId] = roller;
        s_results[roller] = ROLL_IN_PROGRESS;
        emit DiceRolled(requestId, roller);
    }
}

fulfillRandomWords function

fulfillRandomWords 함수는 VRFConsumerBaseV2 컨트랙트 내에 정의된 특수 함수입니다. coordinator는 생성된 randomWords의 결과를 다시 fulfillRandomWords로 보냅니다. 여기서 받아온 결과를 처리하기 위해 몇가지 기능을 추가로 구현합니다.

  1. 결과를 1에서 20사이의 숫자로 변경합니다. randomWords는 여러 임의의 값을 포함할 수 있는 배열이라는 점에 유의해야합니다. 이 예제에서는 임의의 값 1개를 요청합니다.
  2. 변환된 값을 s_results 매핑 변수의 주소에 할당합니다.
  3. 그리고 DiceLanded Event를 발생시킵니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";

contract VRFD20 is VRFConsumerBaseV2 {
    // ...
    // { variables }
    // ...

    // events
    // ...
    event DiceLanded(uint256 indexed requestId, uint256 indexed result);

    // ...
    // { constructor }
    // ...

    // ...
    // { rollDice function }
    // ...

    // fulfillRandomWords function
    function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {

        // transform the result to a number between 1 and 20 inclusively
        uint256 d20Value = (randomWords[0] % 20) + 1;

        // assign the transformed value to the address in the s_results mapping variable
        s_results[s_rollers[requestId]] = d20Value;

        // emitting event to signal that dice landed
        emit DiceLanded(requestId, d20Value);
    }
}

house function

마지막으로 houser 함수는 주소의 house 값을 반환합니다.

house 이름 목록을 가지려면 house 함수에서 호출되는 getHouseName 함수를 만듭니다.

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

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";

contract VRFD20 is VRFConsumerBaseV2 {
    // ...
    // { variables }
    // ...

    // ...
    // { events }
    // ...

    // ...
    // { constructor }
    // ...

    // ...
    // { rollDice function }
    // ...

    // ...
    // { fulfillRandomWords function }
    // ...

    // house function
    function house(address player) public view returns (string memory) {
        // dice has not yet been rolled to this address
        require(s_results[player] != 0, "Dice not rolled");

        // not waiting for the result of a thrown dice
        require(s_results[player] != ROLL_IN_PROGRESS, "Roll in progress");

        // returns the house name from the name list function
        return getHouseName(s_results[player]);
    }

    // getHouseName function
    function getHouseName(uint256 id) private pure returns (string memory) {
        // array storing the list of house's names
        string[20] memory houseNames = [
            "Targaryen",
            "Lannister",
            "Stark",
            "Tyrell",
            "Baratheon",
            "Martell",
            "Tully",
            "Bolton",
            "Greyjoy",
            "Arryn",
            "Frey",
            "Mormont",
            "Tarley",
            "Dayne",
            "Umber",
            "Valeryon",
            "Manderly",
            "Clegane",
            "Glover",
            "Karstark"
        ];

        // returns the house name given an index
        return houseNames[id - 1];
    }
}

이제 랜덤한 값을 생성하고 사용자에게 왕조의 게임 House를 할당하는데 필요한 모든 기능을 완료했습니다. 컨트랙트를 더 쉽고 유연하게 사용할 수 있도록 몇 가지 helper 함수를 추가했습니다.

How do i deploy to testnet?

  • 배포 시 생성자에 Subscription ID를 매개변수를 전달합니다.

How do i add my contract to my subscription account?

컨트랙트를 배포한 후에는 consumer contract로 추가해야 랜덤 값을 요청할 때 Subscription 잔액을 사용할 수 있습니다. Subscription Manager로 이동하여 배포한 컨트랙트 주소를 Consumer 목록에 추가합니다.

How do i test rollDice?

이더리움 주소를 매개변수로 “rollDice”함수를 호출합니다.

트랜잭션이 완료되고 rollDice에 전달된 주소로 house 함수를 호출하면 house를 받을 수 있습니다.

용어


Oracle

블록체인 및 스마트 컨트랙트와 외부 세계 간의 정보를 중개하는 서비스 또는 프로토콜을 의미한다.

profile
좋은 개발자가 되고싶은

0개의 댓글