Getting started with Liquidity: coinflip

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

July 31st 2018: I’ve updated the article for the current betanet.

I wanted to write about Liquidity earlier this year, but the development story wasn’t very convenient yet. One of the reasons was the small maximum operation size, which proved too restrictive for Liquidity contracts. In the betanet release, the constants are much more forgivable. 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.

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.

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.

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 = {
  k : key;
  h : bytes;
  po : nat option;
}

Variant types always contain just one of the possible variants.

type p =
  | Register of bytes
  | Preimage of nat
  | Resolve of unit

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

let%entry main
    (parameter : p)
    (storage : s)
  : operation list * 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 – a list of internal operations 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. A good approach is to write your code first, and only afterwards use functions if you find yourself repeating pieces of code often. Functions can take up a lot of space in the compiled Michelson, especially if they have many arguments.

You can now try to define the storage type, 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 how the contract will be interacted with. We can imagine three different scenarios – 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 bytes
  | Preimage of nat
  | 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. The hash will later be used to verify the provided number. To identify the players later on we will use Current.sender (). () is a literal of unit, a placeholder type for when we don’t need any type (similar to void in other languages).

Current.sender gives us an address, which can be compared to other addresses. If we want to transfer to the contract at that address, we have to let the compiler know what parameter type it should expect with Contract.at. If the address contains a contract with another parameter type, we will get a runtime error.

Once two players have registered, they will reveal their numbers. The numbers will be checked against the stored hash and saved to storage. We can use the previously saved addresses to make sure only the registered players can reveal their number.

The input type for resolving our contract is the unit we mentioned above.

To manipulate storage, we just modify the existing storage variables, or create a new storage from scratch and return it alongside the internal operations. 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 bytes, which is why we are using numbers to decide the winner. We 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 may 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. You can also use the Debug tab to observe the stack during execution. Another good way to get a sense of what’s going on is to follow the stack changes with the Michelson mode for emacs.

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 for nothing 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.

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.

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.

tzsuite: event driven library for Tezos

October 2, 2018
golang tezos michelson cryptocurrency blockchain smart contracts applications oracles automation

Getting started with Liquidity 2: authentication

July 31, 2018
easy liquidity cryptocurrency tezos michelson blockchain smart contracts

Tezos changes: zeronet update

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