Factori, for OCaml users

Tezos Blockchain OCaml Ligo Smart Contract
Published on 2022/07/07
 Factori, for OCaml users


Last edit 01/09/2022

Introduction

Smart contracts in the Tezos Blockchain are written in a low-level language called Michelson. Writing code in Michelson may quickly become tedious; it is like programming your daily tools with Assembly. However, multiple top-level languages generate Michelson, such as Ligo, Morely, and SmartPy, making smart contracts writing very productive and enjoyable.

This tutorial will use Ligo (but not directly) to generate Michelson code. Ligo accepts multiple syntaxes as input: JsLigo, ReasonLigo, PascaLigo, and CameLigo.

As the title suggests, we will write our smart contracts using OCaml with some added annotations and then generate a Ligo file using Mligo, a tool and a library that allow users to develop smart contracts under an OCaml environment (Tuareg, Merlin, Dune, Qcheck, etc.). After, we generate a Michelson file using Ligo and import this Michelson code inside Factori to interact with it.

Installation

To write smart contracts, we need to install Mligo, Ligo, and Factori.

Mligo

Mligo is not available in opam yet, but you can install it using:

$ opam pin add mligo.~dev git+https://gitlab.com/functori/dev/mligo

(say yes to the prompts and type eval $(opam env) afterwards).

Mligo also contains a preprocessor to handle annotations and select the correct code before generating the Ligo file. To add this preprocess to your project, you need to add the command pps mligo.ppx to your preprocess stanza as shown below:

(library
  ...
  (preprocess (pps mligo.ppx))
  ...
)

You will also need to start your smart contract with

open Mligo

Ligo

All installation instructions are explained on the official website of Ligo.

Factori

A tutorial is online which introduces Factori. It explains how to install Factori and interact with smart contracts using TypeScript. The installation part works for both OCaml and TypeScript output.

Example 1: Perfect numbers

The first smart contract that we will create is a simple number validator. First, a user (or a smart contract) will propose a number with a specific property (such as Prime Numbers, Perfect Numbers, Fibonacci number, etc.). If the provided number satisfies that property and exceeds the current number, then we reward the user with some XTZ. Otherwise, we fail the call of the smart contract.

The property chosen here is Perfect Number. A perfect number is a number which is equal to the sum of its divisors excluding itself. More information about this property can be found here.

To create our smart contract, we need to provide at least two pieces of information:

  • The type of the storage
  • The main entrypoint function.

Let's create our contract perfect_number.ml in a new folder.

In order to benefit from the merlin tool, let's create a dune file right away:

(library
  (name perfect_number)
  (modules perfect_number)
  (preprocess (pps mligo.ppx))
)

We also need a dune-project file or dune will complain, all in the same folder:

(lang dune 2.9)

For instance, for our first example, let's create a dune file

Storage

As the logic behind our contract indicates, we need to check if the provided number is greater than the current number. This leads to having storage with a simple type int:

type storage = int

Main entrypoint

The main entrypoint of a smart contract is a function that takes a specific type as an argument and returns the list of operations done by the smart contract and the state of the new storage. Here is its signature:

val main : params * storage -> operation list * storage

(not included in the actual smart contract).

where we define:

type params = Propose of int [@@entry Main]

Since our smart contract contains only one entrypoint, we have only one constructor Propose in the params type.

The implementation of the main function will then have one case:

let main (action, store : params * storage) : operation list * storage =
  match action with
  Propose number -> (* handle entrypoint here. *)

Let's now implement the logic of the Propose entrypoint:

Checking the property

This function will just verify the property: "n equals the sum of its divisors." It should be optimized since every operation running on the Tezos Blockchain costs some XTZ:

let rec sum (acc : int) (n : int) (i : int) : int =
  if i > n / 2 then
    acc
  else if n mod i = 0n then
    sum (i + acc) n (i + 1)
  else
    sum acc n (i + 1)

let perfect_number (n : int) : bool =
  sum 0 n 1 = n

Note that the type of n mod i is nat in Ligo, hence the 0n which will only understood by the OCaml compiler with the help of the preprocessor. Another issue you can face with Ligo is getting recursive functions to typecheck, since they need to be tail-recursive.

The entrypoint itself

This function treats the entrypoint itself, and it should be called from the main entrypoint (main function):

let play (number, store : int * storage) =
  if store < number then
    if perfect_number number then
      (* get the contract from the source address. *)
      let source : (unit, storage) contract =
        Tezos.get_contract_with_error None (Tezos.get_source None) "Source address not found." in
      (* reward the user with 2000mutez. *)
      let transaction : operation =
        Tezos.transaction None unit 2000u source in
      (* add the reward transaction the list of operations. *)
      ([transaction], number : operation list * storage)
    else
      (failwith "The number you provided it not a perfect number." : operation list * storage)
  else
    (failwith "The number you provided is smaller than the current perfect number." : operation list * storage)
Used functions
  • Tezos.get_source: it returns the source address that called the contract. If a tz1 called a smart contract kt1 and this kt1 called another kt1', the source address inside kt1' will be the first tz1.
  • Tezos.get_contract_with_error: it takes an address and returns the associated contract. Another equivalent function could be Tezos.get_contract_opt which behaves like Tezos.get_contract_with_error but returns (params, storage) contract option.
  • Tezos.transaction: it makes a transfer (or a smart contract call) by providing the parameters of the entrypoint (if it is a contract), the amount (int tez or mutez) and the target address.

Note that every function used with Mligo (and all recursive functions) should have a type annotation. Otherwise, Ligo will not be able to typecheck the generated file. (This is true even of the failwith instructions, which need to be given the type they are circumventing by raising an error). Also, the first argument of the functions of the Tezos module is a context used to make tests, and it is None in our case (this is proper to Mligo only, and this argument will be deleted before the generation of the ligo file).

Final smart contract file:

open Mligo

type storage = int

type params = Propose of int
[@@entry Main]

(* checking property. *)
let rec sum (acc : int) (n : int) (i : int) : int =
  if i > n / 2 then
    acc
  else if n mod i = 0n then
    sum (i + acc) n (i + 1)
  else
    sum acc n (i + 1)

let perfect_number (n : int) =
  sum 0 n 1 = n

(* propose entrypoint. *)
let play (number, store : int * storage) =
  if store < number then
    if perfect_number number then
      let source : (unit, storage) contract =
        let source : address = Tezos.source None in
        Tezos.get_contract_with_error None source "Source address not found." in
      let transaction : operation =
        Tezos.transaction None unit 2000u source in
      ([transaction], number : operation list * storage)
    else
      (failwith "The number you provided it not a perfect number." : operation list * storage)
  else
    (failwith "The number you provided is smaller than the current perfect number." : operation list * storage)

(* main entrypoint. *)
let main (action, store : params * storage) : operation list * storage =
  match action with
  | Propose number -> play (number, store)

You can already run dune build to check that it compiles.

Generate Ligo file

Now that we have written our smart contract in OCaml, we can generate a Ligo file (with the extension .mligo in our case):

$ to_mligo perfect_number.ml

(generates a perfect_number.mligo file).

We can make this automatic by adding some stanzas in our dune file:

(library
  (name perfect_number)
  (modules perfect_number)
  (preprocess (pps mligo.ppx))
)

(rule
  (deps perfect_number.ml)
  (targets perfect_number.mligo)
  (action (run to_mligo perfect_number.ml))
)

(you might need to delete perfect_number.mligo before dune build works again, because it won't want to overwrite your existing file).

Generate Michelson file

To get the Michelson file from the generated file perfect_number.mligo:

$ ligo compile contract perfect_number.mligo > perfect_number.tz

If you used the rule in the above dune file to generate perfect_number.mligo, then it is in _build/default/ and you need to run

$ ligo compile contract _build/default/perfect_number.mligo > perfect_number.tz

Once again, let's make this more automatic by adding a rule to our dune file:

(rule
    (deps perfect_number.mligo)
    (targets perfect_number.tz)
    (action (with-stdout-to perfect_number.tz (run ligo compile contract perfect_number.mligo -e main)))
)

(once again, you might need to delete perfect_number.tz before dune build works again, because it won't want to overwrite your existing file).

Click here to display the content of perfect_number.tz

{ parameter int ;
  storage int ;
  code { UNPAIR ;
         DUP ;
         DIG 2 ;
         COMPARE ;
         LT ;
         IF { DUP ;
              PUSH int 1 ;
              DUP 3 ;
              PUSH int 0 ;
              PAIR 3 ;
              LEFT int ;
              LOOP_LEFT
                { UNPAIR 3 ;
                  PUSH int 2 ;
                  DUP 3 ;
                  EDIV ;
                  IF_NONE { PUSH string "DIV by 0" ; FAILWITH } {} ;
                  CAR ;
                  DUP 4 ;
                  COMPARE ;
                  GT ;
                  IF { SWAP ; DIG 2 ; DROP 2 ; RIGHT (pair int int int) }
                     { PUSH nat 0 ;
                       DUP 4 ;
                       DUP 4 ;
                       EDIV ;
                       IF_NONE { PUSH string "MOD by 0" ; FAILWITH } {} ;
                       CDR ;
                       COMPARE ;
                       EQ ;
                       IF { PUSH int 1 ; DUP 4 ; ADD ; DUG 2 ; DIG 3 ; ADD }
                          { PUSH int 1 ; DIG 3 ; ADD ; DUG 2 } ;
                       PAIR 3 ;
                       LEFT int } } ;
              COMPARE ;
              EQ ;
              IF { PUSH string "Source address not found." ;
                   SOURCE ;
                   CONTRACT unit ;
                   IF_NONE { FAILWITH } { SWAP ; DROP } ;
                   PUSH mutez 2000 ;
                   UNIT ;
                   TRANSFER_TOKENS ;
                   SWAP ;
                   NIL operation ;
                   DIG 2 ;
                   CONS ;
                   PAIR }
                 { DROP ;
                   PUSH string "The number you provided it not a perfect number." ;
                   FAILWITH } }
            { DROP ;
              PUSH string "The number you provided is smaller than the current perfect number." ;
              FAILWITH } } }

Factori

Now that we have a Michelson file. We will use Factori to import it and deploy it to the Tezos Blockchain.

To import a Michelson file in factori:

$ factori import michelson perfect_number perfect_number.tz --name pn --ocaml --force

If you used the rule in the above dune file to generate perfect_number.mligo, then it is in _build/default/ and you need to run

$ factori import michelson perfect_number _build/default/perfect_number.tz --name pn --ocaml --force

It will create a new project folder named perfect_number:

perfect_number
└── src
    ├── libraries
    │   ├── blockchain.ml
    │   ├── dune
    │   ├── factori_types.ml
    │   └── utils.ml
    ├── ocaml_scenarios
    │   ├── dune
    │   └── scenario.ml
    └── ocaml_sdk
        ├── dune
        ├── pn_code.ml
        ├── pn_code.mli
        ├── pn_ocaml_interface.ml
        └── pn_ocaml_interface.mli

Successfully imported KT1.

It contains folders such as libraries that include several OCaml modules to interact with the blockchain, ocaml_sdk contains the code of the smart contract that you just imported and its OCaml interface (type definitions, storage access, entrypoint calls, contract deployment), and ocaml_scenarios contains an empty scenario file you need to fill if you want to have a scenario inside the factori project.

Scenario

Below we write our scenario in perfect_number/src/ocaml_scenarios/scenario.ml.

Note that before our scenario can compile and run, we need to install the needed OCaml dependencies:

make -C perfect_number deps
make -C perfect_number ocaml

Whenever you want to run your scenario, you can simply run:

make -C perfect_number run_scenario_ocaml

Now let's deploy our contract inside the scenario. The function deploy is available in the Pn_ocaml_interface module:

open Pn_ocaml_interface
open Tzfunc.Rp

let main () =
  let>? perfect_number_kt1,_op_hash =
    deploy
      ~node:Blockchain.ithaca_node
      ~name:"perfect_number"
      ~from:Blockchain.alice_flextesa
      ~amount:100000L
      Z.one in

  Format.printf "KT1: %s@." perfect_number_kt1;
  Lwt.return_ok ()

let _ =
  Lwt_main.run (main ())

This will deploy the contract to the ithacanet node from the user called alice_flextesa (a tz1 address used initially by the flextesa sandbox, but it is also available in the ithacanet).

Note that the value of the initial storage is a Z.t since int in Michelson is a Z.t even if we defined our storage as int before we generate the contract. Also, our smart contract rewards its users if they provide a correct number; that is why we added 100000 mutez to its balance.

We have deployed our contract; let's interact with it by calling its entrypoint:

(* ... *)
  let>? operation_hash =
    call__default
      ~node:Blockchain.ithaca_node
      ~from:Blockchain.bob_flextesa
      ~kt1:perfect_number_kt1
      (Z.of_int 28)
  in
  Format.printf "Operation Hash: %s@." operation_hash
(* ... *)

If the smart contract contains only one entrypoint, it will have the name call__default. This call will check if the number is greater than the current number (the one inside the smart contract storage) and then check if it is a perfect number. If both conditions are satisfied, then it will return the hash of the operation, and you can search for that hash by using an explorer such as tzkt.io(make sure that you are in the right network). If one of the conditions is not satisfied, then the call to the smart contract will fail, and the node will return an error according to your case:

  • If the number is not greater than the current perfect number:
[Error in forge_manager_operations (call_entrypoint)]: {
  "kind": "node_error",
  "errors": [
    {
      "kind": "temporary",
      "id": "proto.012-Psithaca.michelson_v1.runtime_error",
      "contract_handle": "KT1FHSwJr8ijrZ8TpXKw76r19McZwhzkgoj8",
      "contract_code": "Deprecated"
    },
    {
      "kind": "temporary",
      "id": "proto.012-Psithaca.michelson_v1.script_rejected",
      "location": 146,
      "with": {
        "string": "The number you provided is smaller than the current perfect number."
      }
    }
  ]
}
  • If the provided number is not a perfect number:
[Error in forge_manager_operations (call_entrypoint)]: {
  "kind": "node_error",
  "errors": [
    {
      "kind": "temporary",
      "id": "proto.012-Psithaca.michelson_v1.runtime_error",
      "contract_handle": "KT1FHSwJr8ijrZ8TpXKw76r19McZwhzkgoj8",
      "contract_code": "Deprecated"
    },
    {
      "kind": "temporary",
      "id": "proto.012-Psithaca.michelson_v1.script_rejected",
      "location": 140,
      "with": {
        "string": "The number you provided it not a perfect number."
      }
    }
  ]
}

While running a scenario with Factori; you may get some debug messages. You can disable them by:

let _ = Tzfunc.Node.set_silent true in
(* your scenario... *)

Final scenario file:

open Pn_ocaml_interface
open Tzfunc.Rp

let main () =
  let _ = Tzfunc.Node.set_silent true in
  Format.printf "Deploying the contract@.";
  let>? perfect_number_kt1, _op_hash =
    deploy
    ~node:Blockchain.ithaca_node
    ~name:"perfect_number"
    ~from:Blockchain.alice_flextesa
    ~amount:10000L
    Z.one
  in
  Format.printf "KT1 : %s@." perfect_number_kt1;
  let>? operation_hash =
    call__default
      ~node:Blockchain.ithaca_node
      ~from:Blockchain.bob_flextesa
      ~kt1:perfect_number_kt1
      (Z.of_int 28)
  in
  Format.printf "[Propose 28] Operation hash: %s@." operation_hash;
  let>? operation_hash =
    call__default
      ~node:Blockchain.ithaca_node
      ~from:Blockchain.bob_flextesa
      ~kt1:perfect_number_kt1
      (Z.of_int 12)
  in
  Format.printf "[Propose 12] Operation hash: %s@." operation_hash;
  Lwt.return_ok ()

let _ =
  Lwt_main.run (main ())

You can find all the files of this example in Factori Examples inside the perfect_number folder. Note that a Makefile is provided.

Example 2: Split or Steal game

In this smart contract, we will implement a known game called Split or Steal (inspired by the prisoner's dilemma). It follows these rules:

  • Two players play for a jackpot.
  • Each player must secretly choose Split or Steal.
  • If both choose Split, they each get a reward (half of the jackpot).
  • If one chooses Split and the other chooses Steal, the one who chooses Steal will win all the jackpot, and the other will get nothing.
  • If both choose Steal, none of the players get a reward.

To implement this smart contract, we will split it into four parts:

  • Registration: Players will enter the game by calling an entrypoint and need to pay fees (the sum of the fees will be the jackpot (or almost)).
  • Playing Both players will provide their answers secretly (i.e. without giving a plain text answer).
  • Revealing Both players must reveal their answers to the smart contract if they played their turns.
  • EndGame Both players have revealed their answers; it is time to reward them (or one of them) according to their answers.

As before, we recommend writing the dune file right away (it will be the same as in the previous example, replacing perfect_number with split_or_steal).

Storage

The storage of this smart contract is a little bit more complicated than the previous one since we need to store multiple information about the players and their choices.

type state = Registration | Playing | Revealing | EndGame

type storage =
{
  player1: address option; (* if a player has not entered the game yet this should be set to None. *)
  player2: address option;
  current_state: state; (* this could be : Registration, Playing, Revealing or EndGame. *)
  choice1_hash: bytes; (* the answer should be hashed. *)
  choice2_hash: bytes;
  choice1_confirm: bool; (* to check if a player has already played. *)
  choice2_confirm: bool;
  choice1: string; (* the answer in raw format. *)
  choice2: string;
  current_players: int; (* number of current players (0, 1 or 2). *)
}
[@@comb]

Main entrypoint

In this smart contract, our main entrypoint does have multiple actions, and it will call the appropriate entrypoint for each case:

let main (action, s: params * storage) : operation list * storage =
  match action with
  | EnterGame -> enter_game ((), s)
  | Play b -> play (b, s)
  | Reveal (a, n) -> reveal((a, n), s)
  | End -> endGame ((), s)

with

type params =
  | EnterGame
  | Play of bytes
  | Reveal of string * string
  | End
[@@entry Main]

Registration

To enter the game, the user must pay the fees (210 mutez, for example). We will use the function Tezos.amount to check if the amount sent by the player is greater or equal 210 mutez:

let enter_game (_, store : unit * storage) =
  if store.current_state = Registration then
    if Tezos.get_amount None < 210u then
      (failwith "Registration fees has to be greater or equal than 210mutez (0.000210tez)" : operation list * storage)
    else
      begin
        if store.current_players = 0 then
          ([] : operation list),
          {
            store with
            player1 = Some (Tezos.get_source None);
            current_players = 1
          }
        else if store.current_players = 1 then
          ([] : operation list),
          {
            store with
            player2 = Some (Tezos.get_source None);
            current_players = 0;
            current_state = Playing
          }
        else
          ([] : operation list), store
      end
  else
    (failwith "The game should be in REGISTRATION phase." : operation list * storage)

We assume that the game starts with Registration phase. We change the state to Playing if there are two registered players.

Playing

This entrypoint will store the hashed answer in the storage to prepare the checking of the answer in the next phase (Revealing) and prevent the same player from providing another answer:

let play (hashed_answer, store : bytes * storage) =
  if store.current_state = Playing then
    if store.player1 = Some (Tezos.get_source None) then
      if store.choice1_confirm then
        (failwith "You have already played." : operation list * storage)
      else
        ([] : operation list),
        {
          store with
          choice1_hash = hashed_answer;
          choice1_confirm = true;
          current_players = store.current_players + 1;
          current_state = if store.current_players = 1 then Revealing else Playing
        }
    else if store.player2 = Some (Tezos.get_source None) then
      if store.choice2_confirm then
        (failwith "You have already played." : operation list * storage)
      else
        ([] : operation list),
        {
          store with
          choice2_hash = hashed_answer;
          choice2_confirm = true;
          current_players = store.current_players + 1;
          current_state = if store.current_players = 1 then Revealing else Playing
        }
    else
      (failwith "You are not registred as a player." : operation list * storage)
  else
    (failwith "The game is in PLAYING phase." : operation list * storage)

If both players played and provided their answers, we change the state to Revealing.

Reveal

The answer that the players need to give to the smart contract needs to be hashed so that they cannot cheat and see what their opponent has played. After, to check the hash, the players need to send their answers with a nonce (to prevent an attack by listing every hash of a possible answer) to the smart contract. Finally, the smart contract will check if the answers are correct by comparing the hash stored from Playing phase and the hash of the answer provided in the Revealing phase:

To create a hash, we could use the sha256 algorithm:

let get_hash ((secret, nonce), _ : (string * string) * storage) : bytes =
  let secret_b, nonce_b = Bytes.pack secret, Bytes.pack nonce in
  let phrase : bytes = Bytes.concat secret_b nonce_b in
  Crypto.sha256 phrase
[@@view]

As you can notice, the annotation [@@view] will let Ligo know that this function is also a view, and it could be called outside the smart contract (to check if the hash generated by the user is correct, for example). We will use this view to check the provided hash inside the reveal entrypoint:

let reveal ((answer, nonce), store : (string * string) * storage) =
  if store.current_state = Revealing then
    begin
      if store.player1 = Some (Tezos.get_source None) then
        if store.choice1_hash = get_hash((answer, nonce), store) then
          ([] : operation list),
          {
            store with
            choice1 = answer;
            current_state = if store.choice2 = "" then Revealing else EndGame
          }
        else
          (failwith "Your (secret, nonce) do not match the stored hash." : operation list * storage)
      else if store.player2 = Some (Tezos.get_source None) then
        if store.choice2_hash = get_hash((answer, nonce), store) then
          ([] : operation list),
          {
            store with
            choice2 = answer;
            current_state = if store.choice1 = "" then Revealing else EndGame
          }
        else
          (failwith "Your (secret, nonce) do not match the stored hash." : operation list * storage)
      else
        (failwith "You are not registred as a player" : operation list * storage)
    end
  else
    (failwith "The game should be in REVEALING phase." : operation list * storage)

When we check the answer, we will also check if the other player has already revealed his answer to change the state to EndGame.

End

If both players registered, played and revealed their choices, someone (even not a registered player) needs to call the endGame entrypoint to reward the appropriate player(s) (we could reward the players when they reveal their choices but it is better to have another entrypoint for that, in order to keep the code clean and easy to understand). Also, to simplify the code, we will create a type for answers:

type answer = Split | Steal | Unknown

with a function that returns the right answer from a string:

let get_answer (s : string) =
  if s = "Split" then Split
  else if s = "Steal" then Steal
  else Unknown

We will also need to reset the storage when the game ends:

let init_storage : storage =
  { current_state = Registration;
    player1 : address option = None;
    player2 : address option = None;
    current_players = 0;
    choice1_hash = Bytes.pack "";
    choice2_hash = Bytes.pack "";
    choice1_confirm = false;
    choice2_confirm = false;
    choice1 = "";
    choice2 = ""; }

and finally, the endGame entrypoint:

let endGame (_, store : unit * storage) =
  if store.current_state = EndGame then
    let p1 : (unit, storage) contract =
        Tezos.get_contract_with_error None
         (Option.unopt store.player1) "Problem with the address of Player 1"
      in
    let p2 : (unit, storage) contract =
        Tezos.get_contract_with_error None
         (Option.unopt store.player2) "Problem with the address of Player 2"
      in
    let ans1 = get_answer store.choice1 in
    let ans2 = get_answer store.choice2 in
    let store : storage = init_storage in
    match ans1, ans2 with
    | Split, Split ->
      let op1 : operation = Tezos.transaction None unit 200u p1 in
      let op2 : operation = Tezos.transaction None unit 200u p2 in
      ([op1; op2], store : operation list * storage)
    | Split, Steal ->
      let op2 : operation = Tezos.transaction None unit 400u p2 in
      ([op2], store : operation list * storage)
    | Steal, Split ->
      let op1 : operation = Tezos.transaction None unit 400u p1 in
      ([op1], store : operation list * storage)
    | Steal, Steal ->
      ([], store : operation list * storage)
    | _, _ ->
      ([], store : operation list * storage)
  else
    failwith "The game should be in END phase."

As you can see, we are not handling the Unknown case in the endGame entrypoint to keep it simple. However, we can imagine a case where the user does not provide the right answer(a string different from "Split" and "Steal"). In this case, we will reward the one who gave a correct answer which leads to:

(* ...the first four cases... *)
| Unknown, Split ->
    let op2 : operation = Tezos.transaction None unit 100u p2 in
      ([op2], store : operation list * storage)
| Unknown, Steal ->
    let op2 : operation = Tezos.transaction None unit 300u p2 in
      ([op2], store : operation list * storage)
| Split, Unknown ->
    let op1 : operation = Tezos.transaction None unit 100u p1 in
      ([op1], store : operation list * storage)
| Steal, Unknown ->
    let op1 : operation = Tezos.transaction None unit 300u p1 in
      ([op1], store : operation list * storage)
| Unknown, Unknown ->
    ([], store : operation list * storage)

Optimization

Our smart contract does not cover every possible case/scenario. For example, one can provide an answer, say "Split", and when the game reaches the revealing phase, the same player checks what the other one played. If the other one chooses "Steal", this player could ruin the game by not revealing his answer to the smart contract, which leads to an infinite pause in the game. A solution could be using the level of blocks (Tezos.get_level) in the blockchain as a timeout or directly Tezos.get_now to get the current timestamp. However, we need to add another entrypoint to trigger this check.

We could also change the storage of the smart contract to be more intuitive:

type player =
{
  addr : address option;
  hash : bytes;
  confirm : bool;
  answer : string;
}

type storage =
{
  current_state : state;
  current_players : int;
  player1 : player;
  player2 : player;
}

The smart contract could be extended to handle multiple players or to have various rounds for the same players. In this case, we just need to reset the storage for every round and change the reward amount regarding the number of rounds players have already played.

Scenario

Our smart contract is ready; let's use Factori to interact with it. If you haven't done so manually already, let's first generate the Michelson file by running

dune build

in our folder (assuming that you produced a dune file similar to the one in the first example).

We will use alice_flextesa and bob_flextesa (two identities available in the Blockchain module and in both flextesa and ithacanet networks) as players. Make sure that you create the project using Factori:

$ factori import michelson split_or_steal split_or_steal.tz --name sos --ocaml --force

(once again, if you used the rule in the dune file to generate split_or_steal.mligo, then it is in _build/default/ and you need to run

$ factori import michelson split_or_steal _build/default/split_or_steal.tz --name sos --ocaml --force

Not that the scenario can indifferently be written inside the Factori generated folders (src/ocaml_scenarios/scenario.ml) or in a split_or_steal_scenario.ml file in our main folder, as is done in the example files.

Let's create a set of variables that could be used several times in our scenario:

let node = Blockchain.ithaca_node in
let alice = Blockchain.alice_flextesa in
let bob = Blockchain.bob_flextesa in
(* ... *)

Deploy

To deploy the smart contract, we need to have an initial storage:

let init_storage =
  {
    player1 = None;
    player2 = None;
    current_players = Z.of_string "0";
    choice1 = "";
    choice2 = "";
    choice1_confirm = false;
    choice2_confirm = false;
    choice1_hash = Crypto.H.mk "";
    choice2_hash = Crypto.H.mk "";
    current_state = Registration
  }

And the deployment should be done the same way as the previous smart contract:

let>? kt1,_op_hash = deploy ~node ~name:"Split-or-Steal" ~from:alice init_storage in

Register

To register Alice and Bob we will use the entrypoint enterGame of the module Sos_ocaml_interface generated by Factori:

let>? alice_op = call_enterGame ~node ~amount:210L ~from:alice ~kt1 () in
Format.printf "Operation hash: %s@." alice_op;
let>? bob_op = call_enterGame ~node ~amount:210L ~from:bob ~kt1 () in
Format.printf "Operation hash: %s@." bob_op;

These two calls are independent, and while running scenarios in the Tezos Blockchain, we need to wait for the confirmation of our operations(wait for the construction of two blocks to get the warranty). We could use the function Blockchain.parallel_calls to have both calls in parallel:

let reg_alice () = call_enterGame ~node ~amount:210L ~from:alice ~kt1 () in
let reg_bob () = call_enterGame ~node ~amount:210L ~from:bob ~kt1 () in
let>? _ =
  Blockchain.parallel_calls
  (Format.printf "Operation Hash : %s@.")
  [reg_alice; reg_bob] in

This way, the two calls may end up in the same block, and we will only wait for two blocks rather than four to have our confirmation. We will use Blockchain.parallel_calls in the rest of the scenario.

Creating a hash

As explained in the Play section of the smart contract, the players need to provide a hash; we can obtain the same hash locally using:

let compute_hash secret nonce =
  let open Tzfunc.Rp in
  let open Factori_types in
  let$ secret_pack = Tzfunc.Forge.pack string_micheline (string_encode secret) in
  let$ secret_nonce = Tzfunc.Forge.pack string_micheline (string_encode nonce) in
  let phrase = Crypto.coerce (secret_pack) ^ (Crypto.coerce secret_nonce) in
  Result.Ok (Digestif.SHA256.digest_bigstring @@ Bigstring.of_string @@ phrase)

let compute_string_hash secret nonce =
  let open Tzfunc.Rp in
  match (let$ hash = compute_hash secret nonce in
         Result.Ok (Digestif.SHA256.to_hex @@ hash)) with
  | Ok hash -> hash
  | _ -> failwith "error in hash computation"

The function compute_string_hash will return the same result as the view get_hash of the smart contract.

Let's create a hash for Alice and Bob:

let alice_choice, alice_secret = "Steal", "3N1C4Y" in
let alice_choice_hash = compute_string_hash alice_choice alice_secret in
let alice_choice_bytes = Crypto.H.mk alice_choice_hash in

let bob_choice, bob_secret = "Split", "04004873" in
let bob_choice_hash = compute_string_hash bob_choice bob_secret in
let bob_choice_bytes = Crypto.H.mk bob_choice_hash in

Play

Now that we have our hash answer for Alice and Bob, let's call the play entrypoint:

let play_alice () = call_play ~node ~from:alice ~kt1 alice_choice_bytes in
let play_bob () = call_play ~node ~from:bob ~kt1 bob_choice_bytes in
let>? _ =
  Blockchain.parallel_calls
  (Format.printf "Operation Hash : %s@.")
  [play_alice; play_bob] in

Revealing the hash

Both players played. It is time to reveal their answers:

let rev_alice () = call_reveal ~node ~from:alice ~kt1 (alice_choice, alice_secret) in
let rev_bob () = call_reveal ~node ~from:bob ~kt1 (bob_choice, bob_secret) in
let>? _ =
  Blockchain.parallel_calls
  (Format.printf "Operation Hash : %s@.")
  [rev_alice; rev_bob] in

Get rewards

Both players revealed their answers; now, everyone can call the endGame entrypoint. We will use Alice since she is the one who won the jackpot:

let>? end_hash = call__end ~node ~from:alice ~kt1 () in
Format.printf "Operation Hash : %s@." end_hash;

Notice that there are some entrypoints with two underscores after call, such as call__default and call__end. Both end and default are OCaml keywords, and Factori needs to change them to prevent type errors in OCaml. This should be fixed soon; for now, users could check the interface of their smart contract and use the proper name of the entrypoint.

Running the scenario

If we compile and run our scenario, a similar output could be:

--------------- Beginning of Split or Steal scenario ---------------

----------------------------- Contract -----------------------------
Deploying the contract...(KT1 : KT1ST6Haqh8pKVcpoLK82i4WvFXXkiJ3Ssse)
--------------------------- Registration ---------------------------
Alice & Bob entering the game...
Operation Hash : oo5cKAay6CEhzVgm7VWqH1HHcJcWyBGD8FpZ7FTAK1K83qNJFtK
Operation Hash : ooZs8RbMfz1S8AJ8MqGfjBytGuZuKQqqWzB3xHnmnEQxfrGr7E7
------------------------------ Playing -----------------------------
Alice & Bob are playing...
Alice playing with the hashed answer :
83685421ee208885a5cada044bec70932213a2eaf3163e3eb0caf8daa0834201
Bob playing with the hashed answer :
c18dcfb4752da291776b460cc4e631c8083170b9adba3c1a51d72ceb0fb89485
Operation Hash : oo4vLSurcLJhcESdqzJh3dWzLj4tDQkMKLxXEyDuYBHhKPVc5Ho
Operation Hash : opD5GD9wFc2nS596jPu4TTxwZgFChgKnfSq9hsqoZftWhs1zBBw
----------------------------- Revealing ----------------------------
Alice & Bob are revealing their choices...
Operation Hash : op9W1oVS4YgJhBgin7S4NgZbeSbLg2KYEATVrL5iKC3BtC38LnC
Operation Hash : ooCeJFg4JavWgeBvgoGyKKWHcjYBGWSLXHyavDFfPz5apyuc4Df
------------------------------ EndGame -----------------------------
Bob ending the game...
Operation Hash : op8Nm5gUFNQDXnvJKgsmdr4h2wN9rtErmpeU91SakPHPei3c78T

------------------ End of Split or Steal scenario ------------------

All the files of the Split or Steal game are available in Factori Examples inside the split_or_steal directory. Note that once again, a Makefile is provided.

Conclusion

We hope you have enjoyed trying out some of our finest tools for OCaml smart contract development: From writing your contract to running a scenario, it can all be done in one step with the help of a Makefile. We look forward to present our upcoming features such as automatic crawling capabilities, and a dynamic web page showing the state of each contract!

|