체인링크 VRF 사용해보기 (3) Subscription과 컨트랙트 연동하기
이전 글 보러가기
체인링크 VRF 사용해보기 (1) 체인링크란?
체인링크 VRF 사용해보기 (2) Subscription Manager 사용하기
컨트랙트 생성 및 배포
이제 Subscription에 컨트랙트를 등록할 것이다. 컨트랙트는 체인링크에서 제공하는 샘플 코드를 조금 변경해서 사용할 것이다.
이 컨트랙트는 VRF를 이용해 요청하는 주소마다 한국의 성씨 20개 중 하나를 랜덤으로 지정해 줄 것이다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
contract VRFD20 is VRFConsumerBaseV2 {
uint256 private constant ROLL_IN_PROGRESS = 42;
VRFCoordinatorV2Interface COORDINATOR;
uint64 s_subscriptionId;
address vrfCoordinator = 0x7a1BaC17Ccc5b313516C5E16fb24f7659aA5ebed;
bytes32 s_keyHash =
0x4b09e658ed251bcafeebbc69400383d49f344ace09b9576fe248bb02c003fe9f;
uint32 callbackGasLimit = 40000;
uint16 requestConfirmations = 3;
uint32 numWords = 1;
address s_owner;
mapping(uint256 => address) private s_rollers;
mapping(address => uint256) private s_results;
event DiceRolled(uint256 indexed requestId, address indexed roller);
event DiceLanded(uint256 indexed requestId, uint256 indexed result);
constructor(uint64 subscriptionId) VRFConsumerBaseV2(vrfCoordinator) {
COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
s_owner = msg.sender;
s_subscriptionId = subscriptionId;
}
function rollDice(
address roller
) public onlyOwner returns (uint256 requestId) {
require(s_results[roller] == 0, "Already rolled");
requestId = COORDINATOR.requestRandomWords(
s_keyHash,
s_subscriptionId,
requestConfirmations,
callbackGasLimit,
numWords
);
s_rollers[requestId] = roller;
s_results[roller] = ROLL_IN_PROGRESS;
emit DiceRolled(requestId, roller);
}
function fulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
) internal override {
uint256 d20Value = (randomWords[0] % 20) + 1;
s_results[s_rollers[requestId]] = d20Value;
emit DiceLanded(requestId, d20Value);
}
function family(address player) public view returns (string memory) {
require(s_results[player] != 0, "Dice not rolled");
require(s_results[player] != ROLL_IN_PROGRESS, "Roll in progress");
return getFamilyName(s_results[player]);
}
function getFamilyName(uint256 id) private pure returns (string memory) {
string[20] memory familyName = [
"Kim",
"Lee",
"Park",
"Choi",
"Jung",
"Kang",
"Jo",
"Yoon",
"Jang",
"Em",
"Han",
"Oh",
"Seo",
"Sin",
"Kwon",
"Hwang",
"Ahn",
"Song",
"Jueon",
"Hong"
];
return familyName[id - 1];
}
modifier onlyOwner() {
require(msg.sender == s_owner);
_;
}
}
코드의 구석구석을 뜯어보자!
uint256 private constant ROLL_IN_PROGRESS = 42;
VRFCoordinatorV2Interface COORDINATOR;
ROLL_IN_PROGRESS
체인링크에 VRF를 요청하면 결과가 나오기까지 시간이 다소 걸린다. 이 때 결과가 나오기까지 대기 상태를 나타내는 변수를 만들어준다.
COORDINATOR
체인링크의 VRFCordinator 인터페이스를 사용할 수 있도록 변수로 선언
uint64 s_subscriptionId;
address vrfCoordinator = 0x7a1BaC17Ccc5b313516C5E16fb24f7659aA5ebed;
bytes32 s_keyHash =
0x4b09e658ed251bcafeebbc69400383d49f344ace09b9576fe248bb02c003fe9f;
uint32 callbackGasLimit = 40000;
uint16 requestConfirmations = 3;
uint32 numWords = 1;
address s_owner;
s_subscriptionId
생성자 때 할당해야 할 subscription id
vrfCoordinator
체인링크에서 구현한 VRFCoordinator. 해당 주소는 뭄바이 테스트넷을 기준으로 만들었으며 다른 체인의 경우 여기서 확인할 수 있다.
s_keyHash
사용할 가스의 최대 가스 가격을 Hash로 지정한다. 이 역시 체인마다 다르기 때문에 다른 체인의 keyHash는 위 주소에서 확인할 수 있다.
callbackGasLimit
callback함수인 fullfillRandomWords의 최대 가스 사용량. 단어 하나당 약 20,000 가스가 드는 걸 참고하여 넉넉하게 잡는 걸 추천한다.
requestConfirmations
요청이 실패했을 경우 최대 재시도 수. 기본값은 3이며 더 높은 값을 설정할 수 있다.
numWords
요청당 받을 수 있는 단어 수. VRFCoordinator.MAX_NUM_WORDS를 초과할 순 없다.
s_owner
onlyOwner modifier를 사용하기 위한 소유자 주소. 오픈제플린에서 제공하는 Ownable 컨트랙트를 Implement 해도 된다.
mapping(uint256 => address) private s_rollers;
mapping(address => uint256) private s_results;
s_rollers
requestRandomWords의 결과값인 requestId에 주사위를 굴린 주소를 매칭한다.
s_results
주사위를 굴린 주소에 랜덤결과값을 매칭한다.
event DiceRolled(uint256 indexed requestId, address indexed roller);
event DiceLanded(uint256 indexed requestId, uint256 indexed result);
DiceRolled & DiceLanded
두 이벤트는 각각 주사위를 굴릴때, 결과값이 나왔을 때 사용된다. 이런 이벤트를 통해 서버 또는 클라이언트단에서 컨트랙트 내의 정보값을 확인할 수 있다.
function rollDice(
address roller
) public onlyOwner returns (uint256 requestId) {
require(s_results[roller] == 0, "Already rolled");
requestId = COORDINATOR.requestRandomWords(
s_keyHash,
s_subscriptionId,
requestConfirmations,
callbackGasLimit,
numWords
);
s_rollers[requestId] = roller;
s_results[roller] = ROLL_IN_PROGRESS;
emit DiceRolled(requestId, roller);
}
rollDice
VRF를 사용하는 핵심 로직인 requestRandomWords가 포함된 메서드. 결과값이 이미 있는 주소라면 트랜잭션이 revert된다. 결과값이 아직 없을 경우 VRFCoordinator에 필요한 정보를 보내 결과값을 확인해줄 수 있는 키값인 requestId를 받는다. 이 requestId에 주사위를 굴린 주소를 매칭한 후, 주사위 굴림 결과 상태를 대기로 바꾼다. 왜 이런 식으로 진행하는지는 아래 메서드를 통해 추가적으로 설명하겠다.
function fulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
) internal override {
uint256 d20Value = (randomWords[0] % 20) + 1;
s_results[s_rollers[requestId]] = d20Value;
emit DiceLanded(requestId, d20Value);
}
fulfillRandomWords
VRFCoordinator의 콜백 함수를 오버라이든 한 것. 우리가 만든 컨트랙트에서 requestRandomWords를 실행하면 체인링크의 VRFCoodinator가 약3-5분 안에(이는 체인에 따라 걸리는 시간이 다를 수 있다.) fulfillRandomWords를 통해 결과값을 도출한다. 이를 오버라이드하여 도출된 결과값을 주사위 굴림 결과 상태에 반영한다.
이해하기 쉽도록 그림으로 표현하자면 이렇다.
function family(address player) public view returns (string memory) {
require(s_results[player] != 0, "Dice not rolled");
require(s_results[player] != ROLL_IN_PROGRESS, "Roll in progress");
return getFamilyName(s_results[player]);
}
family
VRF가 성공적으로 실행된(=굴린 주사위 결과값이 나온) 이후 주사위를 굴렸던 주소의 결과값을 확인할 수 있다. 주사위를 굴린 적이 없거나 아직 결과가 안 나온 상태라면 트랜잭션이 실패한다.
function getFamilyName(uint256 id) private pure returns (string memory) {
string[20] memory familyName = [
"Kim",
"Lee",
"Park",
"Choi",
"Jung",
"Kang",
"Jo",
"Yoon",
"Jang",
"Em",
"Han",
"Oh",
"Seo",
"Sin",
"Kwon",
"Hwang",
"Ahn",
"Song",
"Jueon",
"Hong"
];
return familyName[id - 1];
}
getFamilyName
일종의 array 상수라고 생각하면 된다. 파라미터로 들어온 index에 따른 값을 반환한다.
modifier onlyOwner() {
require(msg.sender == s_owner);
_;
}
onlyOwner
컨트랙트 소유자만 실행할 수 있도록 하는 modifer. 소유자가 아닐 경우 트랜잭션이 실패한다.
컨트랙트의 코드를 모두 살펴봤으니 remix를 이용해 배포해보자. 이때, 2편에서 subscription을 만들 때 사용하던 주소로 배포해야 한다.
배포가 완료되면 생성했던 체인링크 VRF Subscription 대시보드(뭄바이 테스트넷)로 돌아가자.
addConsumer
addConsumer 버튼을 눌러 배포한 컨트랙트 주소를 추가한다. 추가된 컨트랙트는 Subscription의 Consumer로서 오라클 오버라이드 메서드를 사용할 때 Subscription에 충전된 LINK를 소비할 것이다.
Consumer를 추가하면 Subscription에 들어가 있는 Consumer 컨트랙트를 확인할 수 있다.
또한 하단의 History에서 추가된 기록을 볼 수 있다.
이제 VRF를 사용할 준비가 끝났다.
VRF 사용하기
VRF를 사용하기 위해 remix에서 컨트랙트 메서드를 실행한다. remix에서 컨트랙트를 배포하면 아래와 같이 메서드 목록이 뜬다.
(+) 입력창에 값을 바로 입력해도 되지만 여러 파라미터를 넣어야 하는 경우, 토글을 눌러 여러 값을 각각의 입력창에 나눠 넣을 수 있다. rollDice의 경우, 파라미터가 하나이기 때문에 입력창이 하나만 뜬다.
rollDice에 아무 주소를 넣고 실행을 하면, 우측의 터미널 창에서 실행 결과 여부를 알려준다. 아래는 성공했을 때 케이스이며, 토글 버튼을 통해 트랜잭션 recipet를 확인할 수 있다.
rollDice를 실행하면 Subscription 대시보드에서 컨트랙트 실행 트랜잭션이 뜬다. Pending이 뜨는 이유는, 컨트랙트가 내부에서 호출한 VRFCoordinator의 작업이 끝나지 않았기 때문이다.
VRFCoordinator에서 작업을 완료하면 Pending이 사라지고 히스토리가 업데이트된다.
이후 remix에서 family 함수를 실행하면 20개의 성씨 중 하나인 Han이 들어왔음을 알 수 있다. VRF를 사용한 것이다!
이렇게 3편에 걸쳐 체인링크 VRF를 사용하는 방법을 알아보았다. 오라클, 그 중에서도 체인링크를 사용해보려는 사람한테 도움이 됐길 바란다.