개인 스터디 차원에서 Chainlink VRF - Overview를 번역하고 이해를 돕고자 내용을 보충하여 정리하였습니다.
Chainlink VRF는 스마트 컨트랙트가 안전하고 사용하기 쉽게 랜덤한 값을 가져올 수 있게 해주는 도구입니다.
Generate Random Numbers for Smart Contracts using Chainlink VRF | Chainlink Documentation
체인링크 VRF v2는 랜덤한 값을 요청하는 두 가지 방법을 제공합니다.
앞서 두가지 방법 중 어떤 방법이 더 나을지는 사용자가 선택해야합니다.
Generate Random Numbers for Smart Contracts using Chainlink VRF | Chainlink Documentation
Chainlink VRF v2 Supported Networks | Chainlink Documentation
Chainlink VRF Contract Addresses | Chainlink Documentation
이 가이드에서는 블록체인에서 랜덤한 값을 생성하는 방법에 대해 알아볼 것입니다. 여기에는 Chainlink Oracle로 Request와 Receive Cycle를 구현하는 방법과 사용자가 만든 스마트 컨트랙트에서 Chainlink VRF로 랜덤을 생성하는 방법을 배우게됩니다.
블록체인에서 랜덤한 값을 생성하기란 매우 어렵습니다. 블록체인의 모든 노드는 동일한 결론에 도달하고 합의를 형성해야 하기 때문입니다. 난수는 다양한 블록체인 애플리케이션에서 유용하지만, 스마트 컨트랙트에서는 기본적으로 생성할 수 없습니다. 이 문제에 대한 해결책은 Chainlink의 검증 가능한 난수 함수 라고도 알려진 Chainlink VRF입니다.
Chainlink의 “Request and Receive” cycle은 블록체인에서 무작위성을 안전하게 활용하기 위한 과정입니다. 기본적으로, 블록체인 자체에서 직접적인 랜덤한 값을 생성하는 것은 안전하지 않습니다. 왜냐하면 그 결과가 블록체인에 저장되면, 어떤 참여자든 그 값을 가져와 결과를 예측할 수 있기 때문입니다.
따라서 Chainlink는 이 문제를 해결하기 위해 “Request and Receive” 사이클을 사용합니다. 이 사이클의 주요 단계는 다음과 같습니다.
이 과정을 통해, 스마트 컨트랙트는 안전하게 무작위 값을 받아올 수 있습니다. 이 값은 예측이나 조작이 불가능하며, Chainlink Oracle에 의해 안전하게 생성되었음을 암호화 증거를 통해 검증할 수 있습니다.
VRF에서 요청(Request)를 하기 위해서는 앞서 말씀드린것과 같이 LINK Token이 필요합니다. LINK Token은 Subscription 계정을 통해 결제를 받습니다. Subscription 관리자를 사용하면 계정을 만들고 VRF 요청에 대한 자금을 선결제할 수 있으므로 모든 애플리케이션 요청의 자금이 한 곳에서 관리됩니다.
설명을 위해 Chainlink VRF를 사용해 랜덤한 값을 생성해내는 애플리케이션을 만들겠습니다.
이 애플리케이션에 사용되는 컨트랙트에는 왕좌의 게임 테마가 있습니다.
컨트랙트가 Chainlink VRF에 무작위한 값을 요청하면, 무작위성 결과는 20면 주사위를 굴리는 것처럼 1에서 20사이의 숫자로 변환됩니다. 각 숫자는 왕좌의 게임 하우스를 나타냅니다. 주사위가 1에 나오면 사용자는 타르가르옌 가문, 2에 나오면 라니스터 가문 등의 가문에 배정됩니다.
주사위를 굴릴 때 주소 변수를 사용하여 각 House에 어떤 주소가 할당되어 있는지를 추적합니다.
컨트랙트에는 다음과 같은 기능이 있습니다.
Chainlink VRF Request는 Subscription 계정에서 자금을 지원받습니다.
Chinlink는 Oracle에서 데이터를 더 쉽게 사용할 수 있도록 컨트랙트 라이브러리를 관리합니다. 체인링크 VRF의 경우 아래와 같은 라이브러리를 사용하게 됩니다.
// 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 {
}
이 예제는 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입니다.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와 주사위의 굴림 결과를 저장하기 위해 사용합니다.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 함수는 다음 작업을 진행합니다.
requestId
와 roller 주소를 저장합니다.주사위가 굴려졌지만 결과가 아직 반환되지 않았음을 알리기 위해 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 함수는 VRFConsumerBaseV2 컨트랙트 내에 정의된 특수 함수입니다. coordinator는 생성된 randomWords의 결과를 다시 fulfillRandomWords로 보냅니다. 여기서 받아온 결과를 처리하기 위해 몇가지 기능을 추가로 구현합니다.
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);
}
}
마지막으로 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 함수를 추가했습니다.
컨트랙트를 배포한 후에는 consumer contract로 추가해야 랜덤 값을 요청할 때 Subscription 잔액을 사용할 수 있습니다. Subscription Manager로 이동하여 배포한 컨트랙트 주소를 Consumer 목록에 추가합니다.
이더리움 주소를 매개변수로 “rollDice”함수를 호출합니다.
트랜잭션이 완료되고 rollDice에 전달된 주소로 house 함수를 호출하면 house를 받을 수 있습니다.
블록체인 및 스마트 컨트랙트와 외부 세계 간의 정보를 중개하는 서비스 또는 프로토콜을 의미한다.