Getting started with Liquidity: coinflip

May 27, 2018
easy liquidity cryptocurrency tezos michelson blockchain smart contracts

Preface

I wanted to write about Liquidity earlier this year, but the development story wasn’t very convenient yet. One of the reasons for this is that the maximum operation size was limited to around 8KB, which proved too small for Liquidity contracts. With the newest iteration of alphanet, the maximum size is 16KB, which is much more reasonable. The online editor for Liquidity has seen some great improvements as well.

In this series, we will explore Liquidity, a high level language for building Tezos smart contracts. Liquidity looks similar to the functional language OCaml, and compiles down to Michelson. We can take our hand written Michelson code and directly compare it with the code produced by the Liquidity compiler, or perhaps take the compiled code and optimize it manually.


Gather the tools

Even though we will be using Liquidity, having a Tezos node set up will prove useful. You can follow the tutorial here to compile an alphanet node (just use git to checkout the alphanet branch instead of the zeronet). If you prefer a more streamlined experience, you can use a docker image.

The best way to write Liquidity is to use the online editor available at the Try-Liquidity website. The editor formats our code automatically and when we try to compile it, we also get feedback about syntax errors, unused variables, etc. After we are happy with the code, we can try running (simulating) the contract or deploying it to the blockchain. Note that simulation of contracts that call other contracts will fail – you will have to deploy them to test them.

It’s a good idea to have the Liquidity documentation open (available in the Help menu in the editor), and perhaps the Michelson documentation as well.


Start with an idea

To get familiar with Liquidity, we will try to implement a simple contract that lets two participants play a game of chance against each other. You can think of it as a coinflip. The standard way to do this would include assigning outcomes to players, generating a random number and doing a modulo 2 to pick the winner. On the blockchain, however, things get a bit more complicated.

The most difficult obstacle is generating the random number securely. Instead of using a random number generator, we will ask the participants to each provide a number and combine them. The result will be used to decide the winner. This approach has some obvious issues – since the algorithm for deciding who wins is public (all smart contracts are published on the blockchain), the second player to provide a number gets to decide the outcome.

We can solve this by requiring players to calculate a hash of a number, and provide that first. After both players have provided a hash, they submit the number itself and the coinflip is resolved. Since hashing is a one way function – it’s hard to recover the input from the output, the players can not cheat by checking the other player’s number before submitting their own. Afterwards, we check whether the provided number produces the provided hash to detect attempts to change the number.


Think about the types

You’ll notice that Liquidity contracts are much easier to read than Michelson contracts.

We can define various types to represent the concepts in our contract. Record types are similar to structs in languages like Java or Go, and contain several named fields.

type player = {
  k2 : key;
  h2 : string;
  po : nat option;
}

Variant types always contain just one of the possible variants.

type p =
  | Register of register
  | Preimage of reveal
  | Resolve of unit

A place of special importance is the entry point (the main function).

let%entry main
    (parameter : p)
    (storage : s)
  : unit * s =
  (* the bulk of our code goes here *)

There’s a few things we have to know here (if you have written Michelson contracts before, some will be familiar):

  1. We have to specify the parameter and storage types here. The names of the types are not important.
  2. The return type of a contract is a tuple of the type of the return value and the storage type.
  3. Most of our code belongs after the equal sign.

We can define types or functions outside the main function, but we will usually call them from the main function. In any case, we won’t be using functions too much, since they take up a lot of space in the compiled Michelson.

You can now try to define the types that we will need, or keep reading to find out how I did it.

What should the storage look like? We have mentioned that our contract will have two participants. We will have to keep some information about each of them – the provided hash, some kind of an address, and the number they provide later on. Let’s have a player object each of the players.

type s = {
  one : player option;
  two : player option;
}

Since the contract starts with no players, we are using the option type together with our custom player type. Option types contain either some value (in this case, data about the player) or a None. We can inspect them with a match statement. After Some, we can create a new variable to hold the inspected value.

(* match statement *)
begin match storage.one with
| None -> (* do something *)
| Some x -> (* do something else *)
end

The type called p hints at what else we will need. The contract will be interacted with in three ways – when players want to register (and provide a hash of their number), when they want to reveal their number, and lastly, when both numbers have been revealed and the contract can be resolved. Only one of those will be needed at the same time, so we can use a variant type to represent all of the possibilities.

type p =
  | Register of register
  | Preimage of reveal
  | Resolve of unit

Variants get compiled to a composition of or types in Michelson. Both records and tuples get compiled to pairs.

At first, the players will pick some number, hash it, and submit the hash along with a public key whose associated secret key they control. The hash will later be used to verify the provided number and the key will be used to pay out the winnings.

type register = {
  k : key;
  h : string;
}

Once two players have registered, they will reveal their numbers. The numbers will be checked against the stored hash and saved to storage. The keys are used again to match players with their record in the storage.

type reveal = {
  k3 : key;
  n: nat;
}

Note that Liquidity currently doesn’t support records containing fields with identical names, so the types look somewhat ugly.

The input type for resolving the contract is the placeholder type unit (similar to void in other languages).


Write the program

When our contract runs, it relies on a match statement to execute different pieces of code for different variants of the parameter. If we are at an unexpected point in the contract’s life cycle, we will throw a failure. Otherwise, we take care of saving players’ information to the storage, updating their information with provided numbers, or calculating a result and paying the winner.

To manipulate storage, we just modify the existing storage variables, or create a new storage from scratch and return it alongside the return value. This is fairly simple, so let’s look at the resolution instead. To combine two numbers we can use the xor instruction. This is not available for strings, which is why we are using numbers to decide the winner. We further divide the result by 2 and look at the remainder. Note that since division by zero is undefined, the operation returns an option type which is None in case of division by zero and Some (result, remainder) for legal division operations. Depending on the remainder, we either pay the winnings to player one or player two.

The provided example contracts use an older version of Liquidity, indicated by the very first line of code. To take advantage of the latest features and fixes, change it to the version shown in the top right.

You can now try to write the code yourself and/or look at my code for this contract. Make use of the Test tab in Liquidity editor to try out different scenarios.


Deploy to the blockchain

When originating (another word for deploying) Michelson contracts, we have to specify an initial storage value. Liquidity lets us specify it directly in the source code, using the init macro:

let%init storage = {
  one = (None : player option);
  two = (None : player option);
}

Let’s deploy the contract and try to use it. Go to Liquidity editor settings and follow the link to the faucet to create a wallet with testnet funds. Activate the account and exit the settings. Alternatively, specify a private key of an account you control.

You can now use the Deploy tab on the right side of the Liquidity editor.

Specify a number of tez to originate the contract with, and click deploy.

The spendable and delegatable options let you specify whether the manager of the contract can spend the funds contained in it. For development we don’t care, but production contracts intented for general use should not be spendable – all the contract’s guarantees are wasted if the funds can be simply extracted. The delegatable flag controls whether the manager is able to set a delegate for the contract. Delegating is a way of supporting a delegate with your voting power in the proof-of-stake system.

After the operation is included in a block, you can check the contract’s storage in the Examine tab, and try calling it in the Call tab.

You can use the tezos-client CLI to generate new key identities and hashes of numbers. tezos-client man shows the whole manual and tezos-client man <command> shows the manual just for that command. The command for getting a new identity is gen keys and for producing a hash it’s hash data.


Try to break it

It’s a good idea to try to break your designs repeatedly during development. You will discover various issues and will be able to fix them sooner.

The contract code I’ve provided above is broken, can you figure out why? In the next article we’ll try to fix some of the issues with it, so you will have a chance to see if you were right.


Don’t miss these resources

If you run into any issues with Liquidity, you can report them at the project’s Github page.

If this article piqued your interest in Tezos, you should come to the developer channel. Other notable places are the Tezos developers google group, tezos.community, Tezos.help, or the official documentation portal.

To get notified about my articles, you can subscribe to my email list.

Tezos changes: zeronet update

April 15, 2018
cryptocurrency tezos michelson blockchain smart contracts applications oracles

Tezos tools: automated contract set up

March 21, 2018
automation cryptocurrency tezos michelson blockchain smart contracts applications oracles golang

Demo: automated insurance

February 8, 2018
demo cryptocurrency tezos michelson blockchain smart contracts applications oracles golang react web