이더리움 체인의 솔리디티를 통하여 가위바위보 게임을 구현하게 되었다. 배팅을 하고, 승부 결과에 따라 배팅된 금액을 번복 없이 보낼 수 있어 구현하기 쉬울 줄 알았다. 컨텐츠로 주어지는 코드를 이해하며 따라 만들어봤다.
- createRoom(가위바위보 입력)
방을 만들면서 방장은 가위바위보를 입력하여 저장한다.- joinRoom(방번호, 가위바위보 입력)
참가 가능한 방에 가위바위보를 입력하여 참가하고, 승패 여부가 결정된다.- payout(방번호)
경기가 끝난 방에, 각 참가 당사자 중 1명이 호출하면 승패 여부에 따라 배팅금액이 지급된다.
승패 여부를 결정 짓고, 각 방에대한 정보 등. 게임에 필요한 여러 함수들도 있지만 실질적으로 컨트랙트 사용자들이 게임에 직접적으로 연관되어 호출할 함수는 위와 같다. 구현은 다음과 같이 됀다.
내가 솔리디티를 공부하면서 예시로 만들어 본 가위바위보 게임에는 문제가 있다.
우선 가위바위보 입력을 0(바위), 1(보), 2(가위) 로 받는다. 이것이 문제는 블록체인에 남는 트랜잭션 기록에 의해서 생긴다.
[문제 사례]
위에서 확인가능한 두 트랜잭션은 각각 createRoom
과 joinRoom
을 호출할 때 블록에 기록된 트랜잭션이다. 그리고 각각의 경우 인자로 유저가 입력한 가위바위보의 값을 확인할 수 있다.
블록체인에 저장되는 데이터는 모두에게 공개된다. 위 트랜잭션이 만약 메인넷에 기록됐으면 누구든지 이 데이터를 확인할 수 있게 된다.
가위바위보 게임을 빗대서 말하자면, 상대방이 어떤 것을 낼지 아는 상태에서 게임이 진행될 수도 있다는 것이다. 당장 위의 트랜잭션을 풀이해보면 0번방의 방장은 0(바위)를 냈고, 이후 참가자는 0번방의 1(보)를 냈다. 배팅된 금액은 당연히 나중에 참가한 참여자가 가져가게 된다.
게임의 결과에 따라 배팅된 금액을 가져감으로서 손실에 직결적으로 연관되는 문제이다 보니까 현재 구현되어 있는 가위바위보 게임은 블록체인에서 사용할 수가 없다. 이를 해결할 수 있는 방법이 없을까??
개인키로 내고 싶은 가위바위보 값에 특정 연산을 하여 해싱한 값을 인자로 넘겨주는 방식이다. 어떻게 구현을 했더라도, 결국 개인키 자체와 가위바위보 값을 해싱하게 되면, 개인키 당 한 번의 가위바위보 게임만 데이터 보안이 보장된다. 왜냐 하면, 결국 개인키와 가위바위보 값은 특정 문자열을 만들어내기 때문에 몇 번 가위바위보를 하고 나면 반복되는 문자열 패턴으로 무엇을 냈는지 예측할 수 있기 때문이다.
근본적인 문제는 해결이 되지 않는다. 블록체인은 트랜잭션도 모두 저장을 하고, 가위바위보 게임처럼 상태값을 바꾸는 함수 호출은 모두 트랜잭션으로 이루어지기 때문이다. 결국 트랜잭션에서부터 값이 노출된다는 것을 간과한 방법이다.
임의로 만든 값으로 결과가 결정되면 게임 참여자들이 이 방식을 수긍할까? 이 방식은 기존 가위바위보를 생각하면 의문이 드는 방식이다. 기존 가위바위보 게임은 본인이 어떤 걸 낼지 자신의 의지가 결과를 만들어낸다고 생각하기 때문이다. 사실 이 방식은 가위바위보 보다는 참여자 각자가 동시에 선택지가 3개 있는 룰렛을 돌리는 방법과 비슷하다.
본인은 3번의 방법 구현을 택했다. 생각 가능한 방법 중에서는 가장 간단하면서도 보안을 신경쓸 필요를 없애는 방법이었기 때문이다.
참여자는 가위바위보가 아닌 자신의 의지대로 만든 숫자열을 입력한다. 모든 참여자 숫자열이 입력이 되면, 컨트랙트에서는 두 참여자 각각에 임의값을 생성하여 입력된 숫자열과 연산을 하게 된다. 해당 숫자열을 3으로 나눠주면 0(바위), 1(보), 2(가위)의 나머지를 추출할 수 있다.
다음은 가위바위보 결과를 판정해주는 프라이빗 함수이다.
[compareHand 함수]
function compareHands(uint roomNum) private {
// 3번 방식 알고리즘 핵심
uint8 originator = uint8(
(uint256(keccak256(abi.encodePacked(block.timestamp,block.difficulty,msg.sender))) + rooms[roomNum].originator.determine) % 3
);
uint8 taker = uint8(
(uint256(keccak256(abi.encodePacked(block.timestamp,block.difficulty,msg.sender))) + rooms[roomNum].taker.determine) % 3
);
rooms[roomNum].gameStatus = GameStatus.STATUS_STARTED;
if(taker == originator){
rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_TIE;
rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_TIE;
emit tie_result(rooms[roomNum].originator.addr, originator, rooms[roomNum].taker.addr, taker);
} else if((taker + 1)% 3 == originator){
rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_WIN;
rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_LOSE;
emit win_result(rooms[roomNum].originator.addr, originator, rooms[roomNum].taker.addr, taker);
} else if((originator + 1) % 3 == taker){
rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_WIN;
rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_LOSE;
emit win_result(rooms[roomNum].taker.addr, taker, rooms[roomNum].originator.addr, originator);
} else {
rooms[roomNum].gameStatus = GameStatus.STATUS_ERROR;
}
}