About Tezos Community Learn Navigate
Tezos Korea Community
About Tezos Community Learn Navigate

리퀴디티 튜토리얼: 무작위 숫자를 뽑는 오라클 게임

번역 제공 (2018/11/23): 테조스 코리아 (원문 링크)

오라클 게임

본 튜토리얼은 테조스 블록체인 상에서 리퀴디티를 이용해 오라클이 무작위 숫자를 내려주는 확률 게임을 만드는 법을 설명합니다.

게임의 원리

테조스 블록체인 상의 스마트 컨트랙트가 게임의 규칙을 관장합니다.

플레이어가 게임을 시작하기로 결정하면, 우선 게임의 스마트 컨트랙트에 0에서 100 사이의(0과 100 포함) 숫자 매개변수(n)를 포함하는 트랜잭션(a call)을 만들어야 합니다. 해당 트랜잭션과 함께 전송되는 숫자가 플레이어의 베팅 금액인 b가 됩니다.

그 후 오라클이 무작위 숫자인 r을 선정하면, 스마트 컨트랙트가 게임의 결과를 결정합니다.
* 만약 플레이어의 숫자인 nr보다 크면 플레이어가 패배하게 됩니다. 이 경우, 베팅한 금액은 몰수되고 게임 스마트 컨트랙트는 재설정됩니다(베팅 금액은 게임 스마트 컨트랙트 상에 보관됩니다.)
* 만약 플레이어의 숫자인 nr보다 작거나 동일할 경우, 플레이어가 승리하게 됩니다. 이 경우, 플레이어는 베팅 금액인 b와 베팅 금액 및 자신이 선택한 숫자에 b * n / 100로 비례하는 상금을 획득합니다. 즉, n을 높게 설정하여 높은 리스크를 감수할수록(오라클이 선정하는 무작위 숫자가 더 높아야 플레이어가 이기므로), 더 높은 상금을 얻을 수 있습니다. 기타 경우에 n = 0인 경우는 항상 이기게 되는 인풋이나 상금은 항상 널(null)값이 되고, n = 100인 경우는 무작위 숫자 역시 100인 경우에만 이길 수 있으나 플레이어는 베팅을 두배로 올려야 합니다.

DApp의 구조

블록체인 상에서 벌어지는 모든 일은 결정론적이며 재현 가능하므로, 스마트 컨트랙트가 무작위 숫자를 안전하게 생성할 수 없습니다.1

이러한 원리에 따라 다음의 스마트 컨트랙트가 작동합니다. 유저가 게임을 시작하면 스마트 컨트랙트는 신뢰할 수 있는 오프 체인 소스로부터 오는 무작위 숫자를 기다리는 상태가 됩니다. 신뢰할 수 있는 소스가 바로 무작위 생성자인 오라클입니다. 오라클은 블록체인을 관찰하다가 스마트 컨트랙트가 무작위 숫자를 기다리고 있다는 것을 감지하면 무작위 번호를 생성하여 스마트 컨트랙트에 전송합니다.
alt text

오라클은 play 트랜잭션이 블록에 포함되기를 기다려서 다음 블록에 무작위 번호를 전송하기 때문에 게임 한 판은 적어도 블록 두개가 생성될 동안 진행됩니다2.

이러한 규정으로 인해 우리는 스마트 컨트랙트를 두 가지 별개의 엔트리 포인트로 분리해야만 합니다:

  1. 첫 번째 엔트리 포인트 play는 게임을 시작하고자 하는 플레이어가 지정합니다(두 번 지정될 수 없음). 엔트리 포인트의 코드는 게임의 매개변수를 스마트 컨트랙트 스토리지에 저장하고 실행을 중지합니다(무작위 숫자를 기다리기 위한 목적).
  2. 오라클만이 지정할 수 있는 두 번째 엔트리 포인트 finish는 무작위 숫자를 매개변수로서 받아들입니다. 두 번째 엔트리 포인트의 코드는 게임의 매개변수 및 무작위 숫자에 근거하여 현재 게임의 결과를 계산하고 이에 따라 진행합니다. finish이 끝나갈 때 즈음 컨트랙트는 재설정되고 새로운 게임이 시작될 수 있습니다.

게임 스마트 컨트랙트

스마트 컨트랙트 게임은 다음 종류의 스토리지를 조작합니다.

type game = {
 number : nat;
 bet : tez;
 player : key_hash;
}

type storage = {
 game : game option;
 oracle_id : address;
}

스토리지에는 오라클의 주소인 oracle_id가 저장되어 있으므로, 해당 주소에서 오는 트랜잭션(즉, 상응하는 개인 키로 서명된 트랜잭션들)만을 받아들입니다. 또한 스토리지에는 현재 게임의 진행 여부를 표시하는 선택 값인 game도 포함됩니다.

한 게임은 다음 세 가지 값으로 구성되며, 이는 기록에 저장됩니다.

  1. number는 플레이어가 선택한 숫자.
  2. bet은 플레이어가 첫 번째 트랜잭션과 함께 전송한 액수로 베팅 금액을 의미함.
  3. player는 키 해시(key hash)로서, 베팅을 한 플레이어가 이길 경우 받고자 하는 금액.
    또한 초기 값으로 컨트랙트를 배포하기 위한 초기화(initializer) 기능도 부여할 수 있습니다. 초기화 기능은 오라클의 주소를 인수로 사용하며, 추후 변경될 수 없습니다.
let%init storage (oracle_id : address) =
      { game = (option); oracle_id }

play 엔트리 포인트

첫 번째 엔트리 포인트인 play는 다음으로 구성된 하나의 쌍을 인수(argument)로 합니다: – 플레이어가 선택한 자연수 – 그리고 플레이어가 상금을 받기를 원하는 주소이자 스마트 컨트랙트의 현재 스토리지인 키 해시(tz1…).

let%entry play (number : nat) storage = ...

해당 컨트랙트는 우선 인풋을 검증합니다:

  1. 숫자가 유효한지, 즉 0에서 100 사이의 숫자인지(자연수는 0이거나 0보다 커야 함) 확인합니다.
if number > 100p then failwith "number must be <= 100";
  1. 플레이어가 이길 경우 컨트랙트가 베팅 금액 및 상금을 지불할 수 있는 충분한 자금이 있는지 확인합니다. 100를 걸 경우, 유저가 원 베팅 금액의 두배를 지불 받으므로 판돈이 가장 커지게 됩니다. 현재 시점에서 컨트랙트 잔금에는 베팅 금액이 입금되므로, 잔금이 베팅 금액의 2배 이상인지를 확인하게 됩니다.
if 2p * Current.amount () > Current.balance () then
  failwith "I don't have enough money for this bet";
  1. 기존 게임이 삭제되지 않게 하기 위해 다른 게임이 진행되지 않도록 확인합니다.
match storage.game with
| Some g ->
  failwith ("Game already started with", g)
| None ->
  (* Actual code of entry point *)

본 엔트리 포인트의 나머지 코드는 단순히 새로운 game 기록인 { number; bet; player }을 생성하고 이를 스마트 컨트랙트의 스토리지에 저장하기 위해 존재합니다. 엔트리 포인트가 컨트랙트 실행 혹은 전송을 하는 것이 아니므로 항상 비어 있는 연산 리스트를 반환합니다.

let bet = Current.amount () in
let storage = storage.game <- Some { number; bet; player } in
(([] : operation list), storage)

이 시점에서 새로운 스토리지는 반환되고 실행은 중단되며 누군가(오라클)가 finish 엔트리 포인트를 지정(call)하기를 기다립니다.

finish 엔트리 포인트

두 번째 엔트리 포인트인 finish는 오라클이 무작위로 생성하는 숫자인 자연수 매개변수 및 스마트 컨트랙트의 현재 스토리지를 인수로 삼습니다.

let%entry finish (random_number : nat) storage = ...

무작위 숫자는 자연수(수학적으로 무한한 자연수) 중 무엇이든 될 수 있으므로 진행하기에 앞서 반드시 숫자가 0과 100 사이의 수임을 확인하여야 합니다. 무작위 숫자가 너무 클 경우 이를 버리지 않고 그저 101로 나누어(유클리드 기하학), 0과 100 사이에 존재하게 되는 나머지 수를 취합니다. 하지만 오라클이 0과 100 사이의 무작위 숫자를 생성하므로, 본 연산은 별 다른 기능은 없고 언젠가 무작위 생성자를 대체하고자 할 때를 위해 보유하면 흥미로울 것입니다.

let random_number = match random_number / 101p with
  | None -> failwith ()
  | Some (_, r) -> r in

스마트 컨트랙트는 테조스 블록체인 상의 공공물이므로 누구든 실행할 수 있습니다. 따라서 스마트 컨트랙트 그 자체의 로직으로 권한을 관리해야 합니다. 특히 누구나 finish 를 실행할 수 있어서는 안 됩니다. 그럴 경우 플레이어가 무작위 숫자를 스스로 선택할 수 있게 되기 때문입니다. 따라서 오라클만이 실행할 수 있도록 만들어야 합니다.

if Current.sender () <> storage.oracle_id then
  failwith ("Random numbers cannot be generated");

또한 무작위 숫자가 유용하게 쓰이려면 현재 진행중인 게임이 있는지 확인하여야 합니다.

match storage.game with
| None -> failwith "No game already started"
| Some game -> ...

본 엔트리 포인트의 나머지 코드는 플레이어의 승패 여부를 결정하고 이에 따라 상응하는 연산을 생성합니다.

if random_number < game.number then
(* Lose *)
([] : operation list)

무작위 숫자가 선택된 숫자보다 작을 경우, 플레이어가 패배합니다. 이 경우 연산이 생성되지 않고 스마트 컨트랙트가 돈을 보관합니다.

else
 (* Win *)
 let gain = match (game.bet * game.number / 100p) with
   | None -> 0tz
   | Some (g, _) -> g in
 let reimbursed = game.bet + gain in
 [ Account.transfer ~dest:game.player ~amount:reimbursed ]

반대의 경우, 즉 무작위 숫자가 기존에 선택된 숫자와 같거나 클 경우 플레이어가 승리합니다. 이 경우 플레이어가 받을 금액을 연산하여 해당 금액(플레이어의 베팅 금액 및 상금)을 지급하고 해당 금액의 전송 연산을 생성합니다.

let storage = storage.game <- (None : game option) in
(ops, storage)

마지막으로 스마트 컨트랙트의 스토리지는 재설정되고 기존 게임은 삭제됩니다. 생성된 연산 리스트 및 재생성된 스토리지는 반환됩니다.

안전 엔트리 포인트: fund

언제든 누가 됐든(대개 컨트랙트의 매니저) 컨트랙트 잔금에 자금을 추가하도록 승인해야 합니다. 이를 통해 컨트랙트에 자금이 고갈되어도 자금을 추가로 입금하여 새로운 플레이어가 게임에 참여할 수 있습니다.

let%entry fund _ storage =
  ([] : operation list), storage

본 코드의 유일한 기능은 해당 금액의 전송을 수락하는 것입니다.

게임 스마트 컨트랙트의 전체 리퀴디티 코드

[%%version 0.403]

type game = {
  number : nat;
  bet : tez;
  player : key_hash;
}

type storage = {
  game : game option;
  oracle_id : address;
}

let%init storage (oracle_id : address) =
  { game = (None : game option); oracle_id }

(* Start a new game *)
let%entry play ((number : nat), (player : key_hash)) storage =
  if number > 100p then failwith "number must be <= 100";
  if Current.amount () = 0tz then failwith "bet cannot be 0tz";
  if 2p * Current.amount () > Current.balance () then
    failwith "I don't have enough money for this bet";
  match storage.game with
  | Some g ->
    failwith ("Game already started with", g)
  | None ->
    let bet = Current.amount () in
    let storage = storage.game <- Some { number; bet; player } in
    (([] : operation list), storage)

(* Receive a random number from the oracle and compute outcome of the
   game *)
let%entry finish (random_number : nat) storage =
  let random_number = match random_number / 101p with
    | None -> failwith ()
    | Some (_, r) -> r in
  if Current.sender () <> storage.oracle_id then
    failwith ("Random numbers cannot be generated");
  match storage.game with
  | None -> failwith "No game already started"
  | Some game ->
    let ops =
      if random_number < game.number then
        (* Lose *)
        ([] : operation list)
      else
        (* Win *)
        let gain = match (game.bet * game.number / 100p) with
          | None -> 0tz
          | Some (g, _) -> g in
        let reimbursed = game.bet + gain in
        [ Account.transfer ~dest:game.player ~amount:reimbursed ]
    in
    let storage = storage.game <- (None : game option) in
    (ops, storage)

(* accept funds *)
let%entry fund _ storage =
  ([] : operation list), storage

오라클

오라클은 운영되는 테조스 노드 상에 Tezos RPCs를 이용해 구현될 수 있습니다. 오라클의 원리는 다음과 같습니다.

  1. 체인 상에 새로운 블록을 관찰합니다.
  2. 새로운 블록이 생기면, 목적지가 게임 스마트 컨트랙트인 성공적인 트랜잭션을 포함하는지 확인합니다.
  3. 트랜잭션의 매개변수가 play, finish 혹은 fund를 호출하는 것인지 확인합니다.
  4. play에 대한 성공적인 호출일 경우, 스마트 컨트랙트가 무작위 숫자를 기다리고 있음을 알 수 있습니다.
  5. 0과 100 사이의 무작위 숫자를 생성하고 게임 스마트 컨트랙트에 적합한 개인 키(예를 들어 트랜잭션은 오라클 서버에 연결된 원장으로 서명될 수 있다)로 호출합니다.
  6. 블록 주기에 따라 다르나, 확인(confirmation)을 위해 조금 기다립니다.
  7. 루프(Loop).

이는 다음의 RPC들과 함께 구현될 수 있습니다:

OCaml에서 무작위 숫자 오라클을 구현하는 방법(트랜잭션을 만들기 위해 리퀴디티 클라이언트를 사용함)은 다음의 저장소에서 보실 수 있습니다:
https://github.com/OCamlPro/liq_game/blob/master/src/crawler.ml.

메인넷 버전 체험하기

본 컨트랙트는 테조스 메인넷 상에 배포되어 있으며, 주소는 KT1GgUJwMQoFayRYNwamRAYCvHBLzgorLoGo입니다. 메인넷 상의 컨트랙트의 경우, 플레이어가 어떤 종류의 피드백도 주지 않을 경우 컨트랙트가 1 μtz를 환불한다는 것이 사소한 차이입니다. Left (Pair 99 “tz1LWub69XbTxdatJnBkm7caDQoybSgW4T3s”) 형태의 매개변수와 함께 트랜잭션(0이 아닌 값으로)을 전송하여 자신의 운을 시험해 보세요. 이 경우 99가 플레이하고자 하는 숫자가 되고 tz1LWub69XbTxdatJnBkm7caDQoybSgW4T3s는 환불 주소입니다. 트랜잭션과 함께 전달 매개변수를 지원하는 월렛(예. Tezbox)을 이용하거나 Tezos 명령행 클라이언트(command line client)을 이용하여 시도하실 수 있습니다.

tezos-client transfer 10 from my_account to KT1GgUJwMQoFayRYNwamRAYCvHBLzgorLoGo --fee 0 --arg 'Left (Pair 50 "tz1LWub69XbTxdatJnBkm7caDQoybSgW4T3s")'

비고

  1. 본 게임의 경우, 오라클을 신뢰하는 것 외에는 다른 방법이 없기 때문에 오라클이 부정을 저지를 수 있습니다. 이러한 결함을 해결하기 위해 무작위 값이 중간 컨트랙트에 저장될 경우 오라클이 무작위 숫자 생성자로 이용될 수 있습니다.
  2. 오라클이 마지막에 생성된 블록 상의 이벤트를 찾을 경우 현재 체인은 폐기되고 무작위 숫자 트랜잭션이 다른 체인에 나타나게 될 수 있습니다. 이 경우, 해당 플레이어가 멤풀(mempool)에서 무작위 숫자를 본다면 기존에 선택한 숫자로 다른 게임을 할 수 있습니다. 실제로는 오라클 연산이 첫 번째 플레이어가 시작한 브랜치에서만 생성되어 다른 브랜치에 들어갈 수 없기 때문에 공격의 위험은 없습니다.

각주


  1. 이더리움 상의 몇몇 컨트랙트는 무작위성을 위해 블록 해시를 사용하나, 블록 해시는 채굴자에 의해 조작되기 쉬우므로 그 이용이 안전하지 않다. 참여자들이 강제되는 약속으로 무작위 숫자의 일부를 기여하도록 하는 다른 방법들도 있다. https://github.com/randao/randao.
  2. 기술적으로는 멤풀(mempool)을 모니터링하여 무작위 숫자를 동일한 블록으로 보낼 수 있으나 좋은 생각은 아니다. 채굴자가 트랜잭션의 순서를 바꿔 두 트랜잭션 모두 실패하게 만들거나 최악의 경우 멤풀(mempool)에서 무작위 숫자를 보고 이에 따라 베팅을 교체할 수 있기 때문이다.

Alain Mebsout: Alain은 OCamlPro의 시니어 엔지니어이다. 2017년 초부터 테조스 ICO 인프라, 특히 비트코인 및 이더리움 스마트 컨트랙트의 설계에 참여하였다. 그 후로 리퀴디티 언어, 컴파일러 및 온라인 에디터를 개발하는 한편, 리퀴디티 스마트 컨트랙트의 검증 업무도 시작하였다. 이 뿐만 아니라 테조스 노드의 코드를 통해 미켈슨 언어의 개선에 기여하였다. Alain은 프로그램의 형식 검증으로 컴퓨터 공학 박사 학위를 받았다.

Replies

댓글 남기기