Commit 4045c3ef authored by Dario Pinto's avatar Dario Pinto
Browse files

add long article + asset and update urls

parent 8f3e6693
title=Liquidity Tutorial: A Game with an Oracle for Random Numbers
authors=Alain Mebsout
date=2018-11-06
category=Blockchains
tags=liquidity,game
# A Game with an oracle
In this small tutorial, we will see how to write a chance game on the Tezos blockchain with Liquidity and a small external oracle which provides random numbers.
## Principle of the game
Rules of the game are handled by a smart contract on the Tezos blockchain.
When a player decides to start a game, she must start by making a transaction (*i.e.* a call) to the game smart contract with a number parameter (let's call it `n`) between 0 and 100 (inclusively). The amount that is sent with this transaction constitute her bet `b`.
A random number `r` is then chosen by the oracle and the outcome of the game is decided by the smart contract.
- The player **loses** if her number `n` is *greater* than `r`. In this case, she forfeits her bet amount and the game smart contract is resets (the money stays on the game smart contract).- The player **wins** if her number `n` is *smaller or equal* to `r`. In this case, she gets back her initial bet `b` plus a reward which is proportional to her bet and her chosen number `b * n / 100`. This means that a higher number `n`, while being a riskier choice (the following random number must be greater), yields a greater reward. The edge cases being `n = 0` is an always winning input but the reward is always null, and `n = 100` wins only if the random number is also `100` but the player doubles her bet.
## Architecture of the DApp
Everything that happens on the blockchain is deterministic and reproducible which means that smart contracts cannot generate random numbers securely <sup>1</sup> .
The following smart contract works in this manner. Once a user starts a game, the smart contract is put in a state where it awaits a random number from a trusted off-chain source. This trusted source is our random generator oracle. The oracle monitors the blockchain and generates and sends a random number to the smart contract once it detects that it is waiting for one.
![](/blog/assets/img/draw_game_arch.jpg)
Because the oracle waits for a `play` transaction to be included in a block and sends the random number in a subsequent block, this means that a game round lasts at least two blocks <sup>2</sup> .
This technicality forces us to split our smart contract into two distinct entry points:
- A first entry point `play` is called by a player who wants to start a game (it cannot be called twice). The code of this entry point saves the game parameters in the smart contract storage and stops execution (awaiting a random number).- A second entry point `finish`, which can only be called by the oracle, accepts random numbers as parameter. The code of this entry point computes the outcome of the current game based on the game parameters and the random number, and then proceeds accordingly. At the end of `finish` the contract is reset and a new game can be started.
## The Game Smart Contract
The smart contract game manipulates a storage of the following type:
```ocaml
type game = {
number : nat;
bet : tez;
player : key_hash;
}
type storage = { game : game option; oracle_id : address; }
```
The storage contains the address of the oracle, `oracle_id`. It will only accept transactions coming from this address (*i.e.* that are signed by the corresponding private key). It also contains an optional value `game` that indicates if a game is being played or not.
A game consists in three values, stored in a record:
- `number` is the number chosen by the player.- `bet` is the amount that was sent with the first transaction by the player. It constitute the bet amount.- `player` is the key hash (tz1...) on which the player who made the bet wishes to be payed in the event of a win.
We also give an initializer function that can be used to deploy the contract with an initial value. It takes as argument the address of the oracle, which cannot be changed later on.
```ocaml
let%init storage (oracle_id : address) =
{ game = (None : game option); oracle_id }
```
### The `play` entry point
The first entry point, `play` takes as argument a pair composed of: - a natural number, which is the number chosen by the player - and a key hash, which is the address on which a player wishes to be payed as well as the current storage of the smart contract.
```ocaml
let%entry play (number : nat) storage = ...
```
The first thing this contract does is validate the inputs:
- Ensure that the number is a valid choice, *i.e.* is between 0 and 100 (natural numbers are always greater or equal to 0).
```ocaml
if number > 100p then failwith "number must be <= 100";
```
- Ensure that the contract has enough funds to pay the player in case she wins. The highest paying bet is to play `100` which means that the user gets payed twice its original bet amount. At this point of the execution, the balance of the contract is already credited with the bet amount, so this check comes to ensuring that the balance is greater than twice the bet.
```ocaml
if 2p * Current.amount () > Current.balance () then
failwith "I don't have enough money for this bet";
```
- Ensure that no other game is currently being played so that a previous game is not erased.
```ocaml
match storage.game with
| Some</span> g ->
failwith ("Game already started with", g)
| None ->
(* Actual code of entry point *)
```
The rest of the code for this entry point consist in simply creating a new `game` record `{ number; bet; player }` and saving it to the smart contract's storage. This entry point always returns an empty list of operations because it does not make any contract calls or transfers.
```ocaml
let bet = Current.amount () in
let storage = storage.game <- Some { number; bet; player } in
(([] : operation list), storage)
```
The new storage is returned and the execution stops at this point, waiting for someone (the oracle) to call the `finish` entry point.
### The `finish` entry point
The second entry point, `finish` takes as argument a natural number parameter, which is the random number generated by the oracle, as well as the current storage of the smart contract.
```ocaml
let%entry finish (random_number : nat) storage = ...
```
The random number can be any natural number (these are mathematically unbounded natural numbers) so we must make sure it is between 0 and 100 before proceeding. Instead of rejecting too big random numbers, we simply (Euclidean) divide it by 101 and keep the remainder, which is between 0 and 100. The oracle already generates random numbers between 0 and 100 so this operation will do nothing but is interesting to keep if we want to replace the random generator one day.
```ocaml
let random_number = match random_number / 101p with
| None -> failwith ()
| Some (_, r) -> r in
```
Smart contracts are public objects on the Tezos blockchain so anyone can decide to call them. This means that permissions must be handled by the logic of the smart contract itself. In particular, we don't want `finish` to be callable by anyone, otherwise it would mean that the player could choose its own random number. Here we make sure that the call comes from the oracle.
```ocaml
if Current.sender () <> storage.oracle_id then
failwith ("Random numbers cannot be generated");
```
We must also make sure that a game is currently being played otherwise this random number is quite useless.
```ocaml
match storage.game with
| None -> failwith "No game already started"
| Some game -> ...
```
The rest of the code in the entry point decides if the player won or lost, and generates the corresponding operations accordingly.
```ocaml
if random_number < game.number then
(* Lose *)
([] : operation list)
```
If the random number is smaller that the chosen number, the player lost. In this case no operation is generated and the money is kept by the smart contract.
```ocaml
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 ]
```
Otherwise, if the random number is greater or equal to the previously chosen number, then the player won. We compute her gain and the reimbursement value (which is her original bet + her gain) and generate a transfer operation with this amount.
```ocaml
let storage = storage.game <- (None : game option) in
(ops, storage)
```
Finally, the storage of the smart contract is reset, meaning that the current game is erased. The list of generated operations and the reset storage is returned.
### A safety entry point: `fund`
At anytime we authorize anyone (most likely the manager of the contract) to add funds to the contract's balance. This allows new players to participate in the game even if the contract has been depleted, by simply adding more funds to it.
```ocaml
let%entry fund _ storage =
([] : operation list), storage
```
This code does nothing, excepted accepting transfers with amounts.
### Full Liquidity Code of the Game Smart Contract
```ocaml
[%%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
```
## The Oracle
The oracle can be implemented using [Tezos RPCs](http://tezos.gitlab.io/mainnet/api/rpc.html) on a running Tezos node. The principle of the oracle is the following:
- Monitor new blocks in the chain.
- For each new block, look if it includes **successful** transactions whose *destination* is the *game smart contract*.
- Look at the parameters of the transaction to see if it is a call to either `play`, `finish` or `fund`.
- If it is a successful call to `play`, then we know that the smart contract is awaiting a random number.
- Generate a random number between 0 and 100 and make a call to the game smart contract with the appropriate private key (the transaction can be signed by a Ledger plugged to the oracle server for instance).
- Wait a small amount of time depending on blocks intervals for confirmation.
- Loop.
These can be implemented with the following RPCs:
- Monitoring blocks: `/chains/main/blocks?[length=<int>]`[https://tezos.gitlab.io/mainnet/api/rpc.html#get-chains-chain-id-blocks](https://tezos.gitlab.io/mainnet/api/rpc.html#get-chains-chain-id-blocks)
- Listing operations in blocks: `/chains/main/blocks/<block_id>/operations/3`[https://tezos.gitlab.io/mainnet/api/rpc.html#get-block-id-operations-list-offset](https://tezos.gitlab.io/mainnet/api/rpc.html#get-block-id-operations-list-offset)
- Getting the storage of a contract: `/chains/main/blocks/<block_id>/context/contracts/<contract_id>/storage`[https://tezos.gitlab.io/mainnet/api/rpc.html#get-block-id-context-contracts-contract-id-storage](https://tezos.gitlab.io/mainnet/api/rpc.html#get-block-id-context-contracts-contract-id-storage)
- Making transactions or contract calls:
- Either call the `tezos-client` binary (easiest if running on a server).
- Call the `liquidity file.liq --call ...` binary (private key must be in plain text so it is not recommended for production servers).
An implementation of a random number Oracle in OCaml (which uses the liquidity client to make transactions) can be found in this repository: [https://github.com/OCamlPro/liq_game/blob/master/src/crawler.ml](https://github.com/OCamlPro/liq_game/blob/master/src/crawler.ml).
### Try a version on the mainnet
This contract is deployed on the Tezos mainnet at the following address:[KT1GgUJwMQoFayRYNwamRAYCvHBLzgorLoGo](https://tzscan.io/KT1GgUJwMQoFayRYNwamRAYCvHBLzgorLoGo), with the minor difference that the contract refunds 1 μtz if the player loses to give some sort of feedback. You can try your luck by sending transactions (with a non zero amount) with a parameter of the form `Left (Pair 99 &quot;tz1LWub69XbTxdatJnBkm7caDQoybSgW4T3s&quot;)` where `99` is the number you want to play and `tz1LWub69XbTxdatJnBkm7caDQoybSgW4T3s` is your refund address. You can do so by using either a wallet that supports passing parameters with transactions (like Tezbox) or the command line Tezos client:
```
tezos-client transfer 10 from my_account to KT1GgUJwMQoFayRYNwamRAYCvHBLzgorLoGo --fee 0 --arg 'Left (Pair 50 "tz1LWub69XbTxdatJnBkm7caDQoybSgW4T3s")'
```
## Remarks
- In this game, the oracle must be trusted and so it can cheat. To mitigate this drawback, the oracle can be used as a random number generator for several games, if random values are stored in an intermediate contract.
- If the oracle looks for events in the last baked block (head), then it is possible that the current chain will be discarded and that the random number transaction appears in another chain. In this case, the player that sees this happen can play another game with a chosen number if he sees the random number in the mempool. In practice, the oracle operation is created only on the branch where the first player started, so that this operation cannot be put on another branch, removing any risk of attack.
**Footnotes**
- Some contracts on Ethereum use block hashes as sources of randomness but these are easily manipulated by miners so they are not safe to use. There are also ways to have participants contribute parts of a random number with enforceable commitments [https://github.com/randao/randao](https://github.com/randao/randao).
- The random number could technically be sent in the same block by monitoring the mempool but it is not a good idea because the miner could reorder the transactions which will make both of them fail, or worse she could replace her bet accordingly once she sees a random number in her mempool.
<hr class="featurette-divider"/>
**Alain Mebsout**: Alain is a senior engineer at OCamlPro. Alain was involved in Tezos early in 2017, participating in the design of the ICO infrastructure and in particular, the Bitcoin and Ethereum smart contracts. Since then, Alain has been developing the Liquidity language, compiler and online editor, and has started working on the verification of Liquidity smart contracts. Alain also contributed some code in the Tezos node to improve Michelson. Alain holds a PhD in Computer Science on formal verification of programs.
......@@ -8,8 +8,6 @@ let old_to_new =
; ("/fr/recrutement-ocamlpro/", "/jobs")
; ( "https://www.ocamlpro.com/pre-inscription-a-une-session-de-formation-inter-entreprises/"
, "/" )
; ( "https://www.ocamlpro.com/2018/11/06/liquidity-tutorial-a-game-with-an-oracle-for-random-numbers/"
, "/" )
; ( "https://www.ocamlpro.com/2019/03/05/signing-data-for-smart-contracts/"
, "/" )
; ( "https://www.ocamlpro.com/2019/03/08/announcing-liquidity-version-1-0/"
......@@ -199,6 +197,9 @@ let old_to_new =
; ( "/2018/10/17/ocamlpros-tzscan-grant-proposal-accepted-by-the-tezos-foundation-joint-press-release/"
, "/blog/2018_10_17_ocamlpros_tzscan_grant_proposal_accepted_by_the_tezos_foundation_joint_press_release"
)
; ( "/2018/11/06/liquidity-tutorial-a-game-with-an-oracle-for-random-numbers/"
, "/blog/2018_11_06_liquidity_tutorial_a_game_with_an_oracle_for_random_numbers"
)
; ( "/2018/11/08/first-open-source-release-of-tzscan/"
, "/blog/2018_11_08_first_open_source_release_of_tzscan" )
; ( "/2018/11/15/an-introduction-to-tezos-rpcs-a-basic-wallet/"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment