Chainlink VRF를 사용해 보자

채동기·2023년 3월 27일
0

Oracle

목록 보기
6/14

Chainlink VRF를 사용하여 Solidity에서 안전하고 검증 가능한 방식으로 무작위 숫자를 생성하는 방법에 대해 알아보겠습니다.

랜덤 숫자는 예측할 수 없는 숫자를 나타냅니다. 그러나 블록체인은 결정론적인 네트워크이기 때문에 블록 타임스탬프, 높이 또는 keccack256 같은 블록체인 기반 데이터를 사용하여 완전히 무작위로 숫자를 생성하는 것은 어렵습니다. 이러한 값들은 조작의 가능성이 있기 때문입니다.

Solidity에서의 무작위 숫자 생성은 시드를 오프체인 리소스 (예: 오라클)에 보내고, 리소스에서 생성된 무작위 숫자와 검증 가능한 증거를 스마트 컨트랙트로 반환받는 방식으로 이루어져야 합니다.

Chainlink VRF를 사용하면 개발자들은 Solidity에서 안전하고 보안성이 높은 무작위 숫자를 쉽게 생성할 수 있습니다.

스마트 컨트랙트에서 안전한 무작위 숫자를 생성하는 예제는 Chainlink 문서에서 찾을 수 있습니다. 이제 테스트를 하고 싶은 사용자를 위해 Kovan 테스트넷에서 블록체인 무작위 숫자 생성의 Remix 예제가 제공됩니다. 단지 요청 및 수신 방법을 따르고 LINK로 스마트 컨트랙트를 자금 조달해야 합니다.

Chainlink VRF 사용하기

Chainlink VRF (Verifiable Random Function)은 스마트 계약에서 안전하고 신뢰성 있는 난수 생성을 위해 설계된 증명 가능하고 검증 가능한 난수 발생기입니다. Solidity 개발자는 이를 사용하여 예측할 수 없는 결과에 의존하는 Ethereum 애플리케이션의 안전하고 신뢰성 있는 스마트 계약을 구축할 수 있습니다.

Chainlink VRF를 사용하여 Solidity에서 난수를 생성하는 첫 번째 단계는 시드를 결정하는 것입니다. 영향을 미치거나 예측하기 어려운 시드를 선택하는 것이 매우 중요합니다. 누군가 시드에 영향을 미치거나 예측할 수 있다면, 이론적으로 랜덤성 요청을 수행하는 오라클과 공모하여 선호하는 결과를 얻을 수 있습니다. 이러한 이유로 블록체인 상태에서 유도된 값(예: 블록 높이 또는 블록 타임스탬프)을 사용하지 않는 것이 좋습니다.

이 시드는 Chainlink 오라클에 대한 요청으로 전송됩니다. 그런 다음 오라클은 지정된 시드로 의사난수를 생성하고, 암호화된 증명과 함께 스마트 계약에 결과를 반환합니다. 이 암호화된 증명은 블록체인 기술의 널리 인정받는 공개키 암호화를 통해 생성됩니다. 결과가 검증 가능하도록 하는 것이 중요합니다. 왜냐하면 마이너나 오라클과 같은 주체가 자신의 이익을 위해 난수 결과에 영향을 미칠 수 있기 때문입니다.

이것은 Chainlink VRF가 작동하는 방식에 대한 개요입니다. 기술적 구현에 대한 자세한 내용은 Chainlink VRF 소개에서 찾을 수 있습니다. 그러나 개발자는 시드를 획득한 후 Chainlink 오라클에 대한 요청을 생성하는 것만 신경 쓰면 됩니다.

Chainlink VRF를 사용하여 예측할 수 없는 결과에 의존하는 모든 애플리케이션에 대해 신뢰할 수 있는 스마트 컨트랙트를 구축할 수 있습니다.

  • 블록체인 게임 및 NFT 구축
  • 임의 할당 작업 및 자원. 예를 들어, 임의로 판사를 사건에 배정
  • 합의 메커니즘을 위한 대표적인 샘플 선택

무작위성을 요청하기 위한 두가지 방법

  • Subscription: 구독 계정을 만들고 LINK 토큰으로 계정을 충전합니다. 사용자는 그런 다음 여러 개의 소비 계약을 구독 계정에 연결할 수 있습니다. 소비 계약이 무작위 값을 요청할 때, 거래 비용은 무작위 요청이 충족된 후 계산되며 구독 잔액에서 차감됩니다. 이 방법을 사용하면 하나의 구독에서 여러 개의 소비 계약 요청에 대한 자금을 조달할 수 있습니다.

  • Direct funding: 소비 계약이 무작위 값을 요청할 때, 소비 계약에서 LINK를 직접 지불합니다. 당신은 소비 계약을 직접 자금을 지원하고, 충분한 LINK 토큰이 무작위 요청을 지불할 수 있도록 해야합니다.

각각의 방법은 상황에 따라 적절한 것을 사용해야 합니다.

VRF 보안 고려 사항

블록체인 내에서 고품질 무작위성을 얻기 위해서는 Chainlink의 VRF와 같은 솔루션을 사용해야 하지만, 채굴자나 검증자가 난수 생성을 조작할 수 있는 방법을 이해해야 합니다. 프로젝트에서 고려해야 할 몇 가지 보안 고려사항은 다음과 같습니다.

  • requestId를 사용하여 순서대로 randomness 요청과 fulfillments을 매칭하세요.
  • 각 블록체인마다 안전한 블록 확인 시간을 선택하세요.
  • 난수를 다시 요청하지 마세요.
  • 난수 요청을 한 후에는 입찰/베팅/입력을 받지 마세요.
  • fulfillRandomWords 함수는 실패하지 않도록 조심하세요.
  • VRF 서비스와 상호 작용하기 위해 VRFConsumerBaseV2를 사용하세요.

requestId를 사용하여 순서대로 randomness 요청과 fulfillments을 매칭하세요.

원칙적으로 기본 블록체인의 채굴자/검증자는 당신의 계약에서의 무작위성 요청을 다른 블록으로 옮겨서 다른 VRF 출력 결과를 얻을 수 있습니다. 이는 채굴자가 미리 무작위값을 결정할 수 있게 하는 것은 아니며, 그저 그들에게 이점을 줄 수도 있고, 줄 수도 없을 뿐입니다. 비유하자면, 그들은 주사위를 다시 굴릴 수 있지만, 어느 면이 나올지 미리 결정하거나 예측할 수는 없습니다.

따라서, 당신이 만드는 무작위성 요청에 적합한 확인 시간을 선택해야 합니다. 확인 시간은 당신의 애플리케이션과 그 가치에 대한 위험성을 고려하여 VRF 서비스가 체인에 충족 내용을 기록하기 전 기다리는 블록 수입니다. 이를 통해 재작성 공격이 불가능하게 만듭니다.

requestId를 사용하여 randomness 요청과 그에 해당하는 fulfillments의 순서를 매칭하는 것이 좋습니다.

만약 여러 개의 VRF 요청이 동시에 이루어지는 경우, VRF fulfillments가 도착한 순서가 사용자의 의미 있는 동작에 영향을 미칠 수 있는지 확인해야 합니다.

각 블록체인마다 안전한 블록 확인 시간을 선택하세요.

블록체인의 마이너/검증자는 요청이 체인 상에서 나타나는 순서와 따라서 계약이 이에 응답하는 순서를 제어할 수 있습니다.

예를 들어, 짧은 시간 내에 randomness 요청 A, B, C를 하였을 때, 이와 관련된 fulfillments가 A, B, C의 순서대로 도착할 것이라는 보장은 없습니다. Randomness fulfillments는 C, A, B 또는 다른 순서로 계약에 도착할 수 있습니다.

따라서, 우리는 requestID를 사용하여 randomness 요청과 그에 해당하는 fulfillments를 매칭하는 것을 권장합니다.

난수를 다시 요청하지 마세요.

randomness가 필요한 경우 VRFv2를 올바르게 사용하려면 임의성 요청을 다시하는 것은 올바르지 않은 사용 방법입니다. 그렇게 하면 VRF 서비스 제공자가 원하는 결과가 나오지 않을 경우 VRF 충족을 보류하고 재요청을 기다려 더 나은 결과를 얻으려는 선택을 할 수 있습니다. 이는 블록 확인 시간과 같은 고려 사항과 유사합니다.

randomness 재요청은 블록체인 상에서 쉽게 감지될 수 있으며, VRFv2를 올바르게 사용하는 경우에는 피해야 합니다.

난수 요청을 한 후에는 입찰/베팅/입력을 받지 마세요.

예를 들어, 사용자의 작업에 응답하여 무작위 NFT를 발행하는 계약을 고려해보면, 계약은 다음과 같이 수행되어야 한다:

사용자의 작업이 생성된 NFT에 영향을 미칠 수 있는 경우 사용자의 작업을 기록한다.
생성된 NFT에 영향을 미칠 수 있는 추가적인 사용자 작업을 받지 않고 무작위성 요청을 발행한다.
무작위성 요청이 이행되면 NFT를 발행한다.

일반적으로, 계약에서 결과가 일부 사용자 입력과 무작위성에 따라 결정되는 경우, 계약은 무작위성 요청을 제출한 후 추가적인 사용자 입력을 받지 않아야 한다.

그렇지 않으면 악성 공격자가 체인을 재작성할 수 있는 경우 암호경제적 보안 속성이 위배될 수 있다.

fulfillRandomWords 함수는 실패하지 않도록 조심하세요.

fulfillRandomWords() 함수가 실패하면, VRF 서비스는 두 번째 호출을 시도하지 않습니다. 따라서 당신의 컨트랙트 로직이 revert되지 않도록 주의해야 합니다. 단순히 랜덤성을 저장하고, 더 복잡한 후속 작업을 당신, 당신의 사용자 또는 자동화 노드가 수행하는 별도의 컨트랙트 호출로 수행하는 것이 좋습니다.

VRF 서비스와 상호 작용하기 위해 VRFConsumerBaseV2를 사용하세요.

만약 구독 방식을 구현한다면, VRFConsumerBaseV2를 사용하세요. 이것은 randomness가 VRFCoordinatorV2에 의해 완료되었는지 확인하는 검사를 포함하고 있습니다. 이러한 이유로, VRFConsumerBaseV2를 상속하는 것이 최선의 방법입니다. 마찬가지로 rawFulfillRandomness를 덮어쓰지 마세요.

계약서에 VRFv2WrapperConsumer.sol을 사용하여 VRF 서비스와 상호 작용하세요.

Direct Funding 방법을 구현하는 경우 VRFv2WrapperConsumer를 사용하십시오. 이 방법은 VRFV2Wrapper에 의해 무작위성이 충족되었는지 확인하는 검사를 포함하고 있습니다. 이러한 이유로 VRFv2WrapperConsumer를 상속하는 것이 좋습니다. 마찬가지로 rawFulfillRandomWords를 무시하지 마세요.

특정 범위에서 난수 구하기

주어진 범위 내에서 난수를 생성해야 하는 경우, modulo를 사용하여 범위를 정의하는 것이 좋습니다. 아래는 1에서 50까지의 범위에서 난수를 얻는 방법입니다.

function fulfillRandomWords(
  uint256, /* requestId */
  uint256[] memory randomWords
) internal override {
  // Assuming only one random word was requested.
  s_randomRange = (randomWords[0] % 50) + 1;
}

여러개의 랜덤 값 가져오기

만약 하나의 VRF 요청으로 여러 개의 난수 값을 얻고 싶다면, numWords 인자를 사용하여 직접 요청할 수 있습니다:

VRF v2 구독 방법을 사용하는 경우, 하나의 요청으로 여러 개의 랜덤 값을 반환하는 예제는 'Get a Random Number' 가이드를 참조하세요.
VRF v2 직접 펀딩 방법을 사용하는 경우, 하나의 요청으로 여러 개의 랜덤 값을 반환하는 예제는 'Get a Random Number' 가이드를 참조하세요.

동시 VRF 요청 처리

동시에 여러 개의 VRF 요청을 처리하려면, requestId와 해당 응답(response) 사이에 매핑(mapping)을 생성하면 됩니다. 또한, 각 requestId를 요청한 주소의 주소(Address)와 매핑(mapping)하여, 어떤 주소가 각 요청을 수행했는지 추적할 수 있습니다.

mapping(uint256 => uint256[]) public s_requestIdToRandomWords;
mapping(uint256 => address) public s_requestIdToAddress;
uint256 public s_requestId;

function requestRandomWords() external onlyOwner returns (uint256) {
  uint256 requestId = COORDINATOR.requestRandomWords(
    keyHash,
    s_subscriptionId,
    requestConfirmations,
    callbackGasLimit,
    numWords
  );
  s_requestIdToAddress[requestId] = msg.sender;

  // Store the latest requestId for this example.
  s_requestId = requestId;

  // Return the requestId to the requester.
  return requestId;
}

function fulfillRandomWords(
    uint256 requestId,
    uint256[] memory randomWords
  ) internal override {
  // You can return the value to the requester,
  // but this example simply stores it.
  s_requestIdToRandomWords[requestId] = randomWords;
}

requestId를 인덱스에 매핑하여 요청한 순서를 추적할 수도 있습니다.

mapping(uint256 => uint256) s_requestIdToRequestIndex;
mapping(uint256 => uint256[]) public s_requestIndexToRandomWords;
uint256 public requestCounter;

function requestRandomWords() external onlyOwner {
  uint256 requestId = COORDINATOR.requestRandomWords(
    keyHash,
    s_subscriptionId,
    requestConfirmations,
    callbackGasLimit,
    numWords
  );
  s_requestIdToRequestIndex[requestId] = requestCounter;
  requestCounter += 1;
}

function fulfillRandomWords(
    uint256 requestId,
    uint256[] memory randomWords
  ) internal override {
  uint256 requestNumber = s_requestIdToRequestIndex[requestId];
  s_requestIndexToRandomWords[requestNumber] = randomWords;
}

다른 실행 경로를 통해 VRF 응답 처리

미리 정해진 조건에 따라 VRF 응답을 처리하려면 열거형(enum)을 만들 수 있습니다. 무작위성을 요청할 때마다 requestId를 열거형(enum)에 매핑합니다. 이렇게 하면 fulfillRandomWords에서 다른 실행 경로를 처리할 수 있습니다. 다음 예를 참조하세요.

// SPDX-License-Identifier: MIT
// An example of a consumer contract that relies on a subscription for funding.
// It shows how to setup multiple execution paths for handling a response.
pragma solidity ^0.8.7;

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

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */

contract VRFv2MultiplePaths is VRFConsumerBaseV2 {
    VRFCoordinatorV2Interface COORDINATOR;

    // Your subscription ID.
    uint64 s_subscriptionId;

    // Sepolia coordinator. For other networks,
    // see https://docs.chain.link/docs/vrf/v2/supported-networks/#configurations
    address vrfCoordinator = 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625;

    // The gas lane to use, which specifies the maximum gas price to bump to.
    // For a list of available gas lanes on each network,
    // see https://docs.chain.link/docs/vrf/v2/supported-networks/#configurations
    bytes32 keyHash =
        0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;

    uint32 callbackGasLimit = 100000;

    // The default is 3, but you can set this higher.
    uint16 requestConfirmations = 3;

    // For this example, retrieve 1 random value in one request.
    // Cannot exceed VRFCoordinatorV2.MAX_NUM_WORDS.
    uint32 numWords = 1;

    enum Variable {
        A,
        B,
        C
    }

    uint256 public variableA;
    uint256 public variableB;
    uint256 public variableC;

    mapping(uint256 => Variable) public requests;

    // events
    event FulfilledA(uint256 requestId, uint256 value);
    event FulfilledB(uint256 requestId, uint256 value);
    event FulfilledC(uint256 requestId, uint256 value);

    constructor(uint64 subscriptionId) VRFConsumerBaseV2(vrfCoordinator) {
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        s_subscriptionId = subscriptionId;
    }

    function updateVariable(uint256 input) public {
        uint256 requestId = COORDINATOR.requestRandomWords(
            keyHash,
            s_subscriptionId,
            requestConfirmations,
            callbackGasLimit,
            numWords
        );

        if (input % 2 == 0) {
            requests[requestId] = Variable.A;
        } else if (input % 3 == 0) {
            requests[requestId] = Variable.B;
        } else {
            requests[requestId] = Variable.C;
        }
    }

    function fulfillRandomWords(
        uint256 requestId,
        uint256[] memory randomWords
    ) internal override {
        Variable variable = requests[requestId];
        if (variable == Variable.A) {
            fulfillA(requestId, randomWords[0]);
        } else if (variable == Variable.B) {
            fulfillB(requestId, randomWords[0]);
        } else if (variable == Variable.C) {
            fulfillC(requestId, randomWords[0]);
        }
    }

    function fulfillA(uint256 requestId, uint256 randomWord) private {
        // execution path A
        variableA = randomWord;
        emit FulfilledA(requestId, randomWord);
    }

    function fulfillB(uint256 requestId, uint256 randomWord) private {
        // execution path B
        variableB = randomWord;
        emit FulfilledB(requestId, randomWord);
    }

    function fulfillC(uint256 requestId, uint256 randomWord) private {
        // execution path C
        variableC = randomWord;
        emit FulfilledC(requestId, randomWord);
    }
}

참조

https://blog.chain.link/random-number-generation-solidity/
https://docs.chain.link/vrf/v2/introduction

profile
what doesn't kill you makes you stronger

0개의 댓글