Techelson

Techelson is a Test Execution Environment (TEE) for Michelson smart contracts. Michelson is the stack-based language used by the tezos blockchain. Techelson is open source and hosted on github, where you can find the build instructions. If you just want to retrieve a binary, head over to releases.

If you have problems or suggestions, help us make Techelson better by opening an issue.

Techelson emulates just enough of the tezos blockchain protocol to be able to create smart contracts and make transfers between contracts. Currently, techelson only aims at testing functional properties of smart contracts. In particular, it does not provide any information about the gas or burn of transfers/contracts. This is because computing the burn, and especially the gas of a transfer is rather complex and would require techelson to drop some of the abstractions it makes over the tezos protocol to run tests faster.

Techelson can be used either as a command-line tool or as an OCaml library. This book focuses on the former use case.

Also, this book assumes the reader is fairly familiar with the michelson language. We will discuss what a contract is and how it behaves, but the reader should know what michelson types and instructions look like, and their semantics.

Michelson is a fairly low-level language which makes it difficult to discuss complex contracts. This book will sometimes give contracts as Liquidity contracts. Liquidity is a higher-level, OCaml-like language for tezos smart contracts which compiles to Michelson.

NB: if you are a Liquidity user, you should probably take a look at this blog post on how to write tests directly in Liquidity, and run these tests using Techelson.

The chapters of this book are

  • Michelson, provides a very brief introduction to michelson smart contracts.
  • Running Tests, describes techelson's workflow for running tests through examples.
  • Test Generation, discusses techelson's test generation features.
  • Quick Reference, quick reminders of techelson's features, such as extensions.

All examples in this book are available in the rsc directory of the github repository.

Michelson

Michelson is the stack-based, strongly typed, low-level language supported by the tezos blockchain for smart contracts. We only provide a brief description of michelson here, and refer the reader to the official documentation for more details.

A michelson contract is similar to transition system. The storage of a contract is its current state; the entry point (code) of a contract is a function which takes

  • some tezos tokens (of type mutez),
  • the current storage of the contract, and
  • a parameter of a certain type provided by the client of the contract.

It returns

  • a list of operations (contract/account creation, transfers, etc.), and
  • the new storage of the contract.

In practice, a contract looks as follows:

storage <type> ;
parameter <type> ;
code <instruction> ;

Note that tokens are passed implicitely: they are credited to the contract before it starts running (although the amount of the transfer can be accessed with the AMOUNT instruction). Hence the code of the contract takes two parameters, which are aggregated in a pair (parameter, storage). The same goes with the operations and the new storage returned by the contract, which are returned as a pair (operations, new_storage).

In the stack-based context of michelson, "takes <something> as argument" means "starts with a stack containing <something>". Likewise, "returns <something>" here means "leaves <something> on the stack at the end (and nothing else than <something>)".

A Simple Example

Let us build a contract which counts how many time it was called. We will allow clients to specify that they do not want to be counted by passing a boolean ghost: if it is true, the contract will not count the transfer.

Based on this description, we already have the storage and parameter types:

storage nat;
parameter bool;

The code of this contract should perform the following steps:

  • destroy the parameter/storage pair
  • branch on the ghost parameter: this consumes the parameter, meaning the storage is now on the top of the stack
    • do nothing if ghost is true: the storage is unchanged
    • add 1 to the storage otherwise
  • pair an empty list of operations with the new storage

The complete description of the contract, simpleExample.tz, is thus

storage nat;
parameter bool;
code {
    UNPAIR;        # Unpair parameter and storage.
    IF {           # Asked not to count: storage is unchanged, nothing to do.
    } {
        PUSH nat 1;
        ADD
    };
    NIL operation; # We don't want to perform any operations.
    PAIR           # Aggregate the operation list and the new storage.
};

Running Tests

Interaction with techelson is currently file-based. There are two kinds of files techelson works with. Contract files are just plain michelson smart contract files, and testcase files are files containing a Michelson instruction, usually a sequence of instructions { <instructions> }.

Omitting options, running techelson looks like (the -- separator is optional):

$ techelson \
    --contract <contract_1> .. --contract <contract_n> \
    -- <testcase_1> ... <testcase_m>

Techelson will then run the testcases in sequence. All testcases will have access to all the contracts provided with --contract. For more information about command-line refer to the Usage section.

This section builds on the small example from the Michelson section, and a slightly more involved example later on, to introduce techelson's workflow and its extended instruction set.

  • First Steps shows how to run a simple testcase with no contract.

    Introduces PRINT_STACK and STEP.

  • Creating and Calling Contracts discusses the contract environment and contract creation and transfers.

    Introduces APPLY_OPERATIONS.

  • Live Contract Inspection deals with recovering the balance and the storage of live (deployed) contracts.

    Introduces GET_BALANCE and GET_STORAGE.

  • Anonymous Contracts details how to create anonymous contracts.

  • Transfers introduces the creation of transfers to a contract.

  • Testing for Failures shows how to test that an operation fails and how. This section is the first to use the slightly more complex example admins.tz, and it's liquidity version admins.liq.

    Introduces MUST_FAIL.

  • Usurpation of Identity illustrates how to have your testcases pretend they are a specific contract/account, and to create operations in their name.

    Introduces SET_SOURCE.

First Steps

Let's give ourselves a testcase file test1.techel

{
    PUSH string "starting the test" ;
}

This testcase does not use any contract. To run it, simply run

$ techelson rsc/no_contract/okay/test1.techel
Running test `Test1`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `Test1`

Introspection

This is not very informative, which is why techelson provides extensions such as PRINT_STACK. This instruction prints the state of the stack in a readable way. For example, if we change the example above to test2.techel to

{
    PUSH string "starting the test" ;
    PRINT_STACK
}

the output becomes

$ techelson rsc/no_contract/okay/test2.techel
Running test `Test2`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| "starting the test"                                                                              |
| string                                                                                           |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `Test2`

Steps

When you run a complex testcase or contract, it can be useful to have break point that stop the execution. This gives you time to read a PRINT_STACK before the next step is actually performed, make one step, read the state of the stack, etc.

The STEP techelson extension allows to do just that. You can also provide a string that will be displayed when the STEP instruction is reached.

The following example (test3.techel) showcases the STEP instruction:

{
    PUSH string "starting the test" ;
    PRINT_STACK ;
    STEP "just started the test" ; # The description string is optional, see below.
    PUSH @not_important string "a string with a variable name" ;
    PRINT_STACK ;
    STEP ; # No description string.
    DROP ;
    PRINT_STACK ;
    STEP "The string should be gone. Also, we're done."
}

Techelson will stop on all STEP instructions and ask you to press enter to keep going:

$ techelson rsc/no_contract/okay/test3.techel
Running test `Test3`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| "starting the test"                                                                              |
| string                                                                                           |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [just started the test] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| "starting the test"                                                                              |
| string                                                                                           |
|--------------------------------------------------------------------------------------------------|
|                                                                                   @not_important |
| "a string with a variable name"                                                                  |
| string                                                                                           |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [no information] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| "starting the test"                                                                              |
| string                                                                                           |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [The string should be gone. Also, we're done.] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `Test3`

Pro tip 1: you can use PRINT_STACK and STEP in contracts too (see the extensions section for details). Also, techelson treats #> as a whitespace. Hence, your can have #> STEP ; and/or #> PRINT_STACK ; in your michelson contract, which lets you inspect it during tests. Your contract remains legal michelson thanks to the leading # which comments the command.

Pro tip 2: passing --skip on to techelson will skip (but still display) all the steps. The output of the commands reported in this book are all obtained by running techelson with --skip on.

Creating and Calling Contracts

When you pass a contract to techelson using techelson --contract <file> ..., the contract becomes a named contract in the techelson environment. The name of the contract is the name of the file

  • up to its first . character,
  • with the first letter capitalized.

So

techelson \
    --contract my_contract.tz \
    --contract myContract.tz  \
    --contract my.contract.tz \
    ...

defines three named contracts: My_contract, MyContract and My.

Note that the naming convention is the same for testcases, based on the testcase file. The name of a testcase might be used in techelson's output to provide information, but it has no practical use currently.

Named Contract Creation

Techelson extends the CREATE_CONTRACT michelson instruction with the following rule

instruction parameter stack
CREATE_CONTRACT <string> :: key_hash : option key_hash : bool : bool : mutez : 'g : 'S
-> operation : address : 'S

where <string> is the name of a contract with storage type 'g in the environment. The semantics of the stack parameters is the same as in michelson: manager, optional delegate, the two spendable and delegatable flags, and the balance and storage of the contract created.

NB: techelson also provides the SPAWN_CONTRACT extension which takes the name of the contract on the stack. See techelson's Extensions for more details.

Say we have the following contract in file simpleExample.tz.

storage nat;
parameter bool;
code {
    UNPAIR;        # Unpair parameter and storage.
    IF {           # Asked not to count: storage is unchanged, nothing to do.
    } {
        PUSH nat 1;
        ADD
    };
    NIL operation; # We don't want to perform any operations.
    PAIR           # Aggregate the operation list and the new storage.
};

We can craft a creation operation in file create1.techel as follows

{
    PUSH @storage nat 0 ;
    PUSH @amount mutez 3 ;
    PUSH @delegatable bool True ;
    PUSH @spendable bool True ;
    NONE @delegate key_hash ;
    PUSH key "manager address" ;
    SHA512 @manager ;
    PRINT_STACK ;
    STEP "before creation" ;

    CREATE_CONTRACT "SimpleExample" ;
    PRINT_STACK ;
    STEP "after creation" ;
}

This produces the following output

$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz -- rsc/simpleExample/okay/create1.techel
Running test `Create1`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
|                                                                                         @storage |
| 0p                                                                                               |
| nat                                                                                              |
|--------------------------------------------------------------------------------------------------|
|                                                                                          @amount |
| 3utz                                                                                             |
| mutez                                                                                            |
|--------------------------------------------------------------------------------------------------|
|                                                                                     @delegatable |
| True                                                                                             |
| bool                                                                                             |
|--------------------------------------------------------------------------------------------------|
|                                                                                       @spendable |
| True                                                                                             |
| bool                                                                                             |
|--------------------------------------------------------------------------------------------------|
|                                                                                        @delegate |
| None                                                                                             |
| (option key_hash)                                                                                |
|--------------------------------------------------------------------------------------------------|
|                                                                                         @manager |
| "sha512:manager address"                                                                         |
| key_hash                                                                                         |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [before creation] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| address[1]                                                                                       |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
| CREATE[uid:0] (@address[1], "sha512:manager address", None, true, true, 3utz) "SimpleExample"    |
| operation                                                                                        |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [after creation] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `Create1`

Applying Operations

Michelson operations (contract/account creation, transfers) cannot be applied directly in a michelson contract. Instead, a contract produces a list of operation which the tezos runtime applies after the contract is done running.

A techelson test case needs to be able to apply operations however, which is why the APPLY_OPERATIONS extension exists. This instruction suspends the execution of the testcase to apply the list of operations on the top of the stack. When all these operations are done running, techelson resumes the execution of the testcase.

Warning: this instruction is only legal in testcases, not in contracts.

Consider testcase create2.techel:

{
    PUSH @storage nat 0 ;
    PUSH @amount mutez 3 ;
    PUSH @delegatable bool True ;
    PUSH @spendable bool True ;
    NONE @delegate key_hash ;
    PUSH key "manager address" ;
    SHA512 @manager ;

    CREATE_CONTRACT @main @main_op "SimpleExample" ;

    DIP { NIL operation } ;
    CONS ;
    PRINT_STACK ;
    STEP "operation is now in a list" ;

    APPLY_OPERATIONS ;

    PRINT_STACK ;
    STEP "testing that contract exists" ;
    # Takes the address on the top of the stack, retrieves a contract of parameter `bool`.
    CONTRACT bool ;
    IF_NONE { # There is no hope, failing.
        PUSH @err_msg string "failed to retrieve contract" ;
        FAILWITH
    } {
        PUSH string "success" ;
        PRINT_STACK
    }
}

Running it yields

$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz -- rsc/simpleExample/okay/create2.techel
Running test `Create2`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
|                                                                                            @main |
| address[1]@main                                                                                  |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
| [ CREATE[uid:0] (@address[1]@main, "sha512:manager address", None, true, true, 3utz) "SimpleExample" ] |
| (list operation)                                                                                 |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [operation is now in a list] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation CREATE[uid:0] (@address[1]@main, "sha512:manager address", None, true, true, 3utz) "SimpleExample"
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: none
=> live contracts: SimpleExample (3utz) address[1]@main

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
|                                                                                            @main |
| address[1]@main                                                                                  |
| address                                                                                          |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [testing that contract exists] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| address[1]@main                                                                                  |
| (contract bool)                                                                                  |
|--------------------------------------------------------------------------------------------------|
| "success"                                                                                        |
| string                                                                                           |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `Create2`

Notice the line Applying operations.... We could increase techelson's verbosity to obtain more information as to which contracts are deployed, but let's see how to inspect the state of a live (deployed) contract instead.

Live Contract Inspection

Michelson smart contracts cannot access each other's storage. They can only interact through transfers, during which the client of the contract provides a parameter that the contract runs its code on.

As a test framework, techelson provides inspection instructions which give access to the balance and the storage of a live (deployed) contract. Both consume a contract from the top of the stack.

instruction parameter stack
GET_STORAGE 'storage :: contract _ : 'S or :: address : 'S
-> (option 'storage) : 'S
GET_BALANCE none :: contract _ : 'S or :: address : 'S
-> mutez : 'S

Let's extend the previous example to inspection.techel which checks that the balance and storage of the contract deployed are correct.

{
    PUSH @storage nat 0 ;
    PUSH @amount mutez 3 ;
    PUSH @delegatable bool True ;
    PUSH @spendable bool True ;
    NONE @delegate key_hash ;
    PUSH key "manager address" ;
    SHA512 @manager ;

    CREATE_CONTRACT @main @main_op "SimpleExample" ;

    DIP { NIL operation } ;
    CONS ;

    APPLY_OPERATIONS ;

    # Takes the address on the top of the stack, retrieves a contract of parameter `bool`.
    CONTRACT bool ;
    IF_NONE { # There is no hope, failing.
        PUSH @err_msg string "failed to retrieve contract" ;
        FAILWITH
    } {} ;

    DUP ;

    GET_BALANCE ;
    PRINT_STACK ;
    STEP "retrieved the balance of the contract" ;

    PUSH mutez 3 ;
    IFCMPNEQ {
        PUSH string "balance should be 3utz" ;
        FAILWITH
    } {} ;

    GET_STORAGE nat ;

    IF_NONE {
        PUSH string "unable to retrieve storage of contract" ;
        FAILWITH
    } {
        PRINT_STACK ;
        STEP "retrieved the storage of the contract" ;
        PUSH nat 0 ;
        IFCMPNEQ {
            PUSH string "storage should be 0 (nat)" ;
            FAILWITH
        } {} ;
    }
}

The testcase does not fail and produces the output

$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz -- rsc/simpleExample/okay/inspection.techel
Running test `Inspection`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation CREATE[uid:0] (@address[1]@main, "sha512:manager address", None, true, true, 3utz) "SimpleExample"
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: none
=> live contracts: SimpleExample (3utz) address[1]@main

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| address[1]@main                                                                                  |
| (contract bool)                                                                                  |
|--------------------------------------------------------------------------------------------------|
| 3utz                                                                                             |
| mutez                                                                                            |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [retrieved the balance of the contract] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| 0p                                                                                               |
| nat                                                                                              |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [retrieved the storage of the contract] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `Inspection`

Anonymous Contracts

Techelson accepts contracts through its --contract option. These contracts are named as discussed in Creating and Calling Contracts. Contracts defined at michelson level in testcases and contracts however are considered anonymous. Anonymous contracts can also be deployed and inspected. In fact, they are not really different from named contracts apart from their lack of name, which (currently) prevent techelson from mentioning where they really come from in its debug output.

The following anonymous.techel testcase is similar to the one from the Live Contract Inspection except that the contract deployed is not given to the environment, it is inlined in the testcase.

{
    PUSH @storage nat 0 ;
    PUSH @amount mutez 3 ;
    PUSH @delegatable bool True ;
    PUSH @spendable bool True ;
    NONE @delegate key_hash ;
    PUSH key "manager address" ;
    SHA512 @manager ;

    CREATE_CONTRACT @main @main_op {
        storage nat;
        parameter bool;
        code {
            UNPAIR ;
            IF {
            } {
                PUSH nat 1;
                ADD
            } ;
            NIL operation ;
            PAIR
        };
    } ;

    DIP { NIL operation } ;
    CONS ;

    APPLY_OPERATIONS ;

    # Takes the address on the top of the stack, retrieves a contract of parameter `bool`.
    CONTRACT bool ;
    IF_NONE { # There is no hope, failing.
        PUSH @err_msg string "failed to retrieve contract" ;
        FAILWITH
    } {} ;

    DUP ;

    GET_BALANCE ;
    PRINT_STACK ;
    STEP "retrieved the balance of the contract" ;

    PUSH mutez 3 ;
    IFCMPNEQ {
        PUSH string "balance should be 3utz" ;
        FAILWITH
    } {} ;

    GET_STORAGE nat ;

    IF_NONE {
        PUSH string "unable to retrieve storage of contract" ;
        FAILWITH
    } {
        PRINT_STACK ;
        STEP "retrieved the storage of the contract" ;
        PUSH nat 0 ;
        IFCMPNEQ {
            PUSH string "storage should be 0 (nat)" ;
            FAILWITH
        } {} ;
    }
}

This produces the exact same output (modulo the testcase's name, and as long as we do not increase verbosity) as for inspection.techel:

$ techelson rsc/no_contract/okay/anonymous.techel
Running test `Anonymous`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation CREATE[uid:0] (@address[1]@main, "sha512:manager address", None, true, true, 3utz) 
                       {
                           storage nat ;
                           parameter bool ;
                           code ...;
                       }
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: none
=> live contracts: <anonymous> (3utz) address[1]@main

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| address[1]@main                                                                                  |
| (contract bool)                                                                                  |
|--------------------------------------------------------------------------------------------------|
| 3utz                                                                                             |
| mutez                                                                                            |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [retrieved the balance of the contract] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| 0p                                                                                               |
| nat                                                                                              |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [retrieved the storage of the contract] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `Anonymous`

Transfers

At this point creating and applying a transfer should be relatively straightforward. Simply create the operation using michelson's TRANSFER_TOKENS, and apply it with APPLY_OPERATIONS. For instance, transfer.techel builds on inspection.techel. It creates an instance of simpleExample.tz, and creates and applies two operations: the first transfers 7 tokens with a parameter equal to False, and the second transfers 13 tokens with True. (Remember that simpleExample.tz will count transfers for which the parameter is False.)

{
    PUSH @storage nat 0 ;
    PUSH @amount mutez 3 ;
    PUSH @delegatable bool True ;
    PUSH @spendable bool True ;
    NONE @delegate key_hash ;
    PUSH key "manager address" ;
    SHA512 @manager ;

    CREATE_CONTRACT @main @main_op "SimpleExample" ;

    ... # Omitting code creating the contract.

    {   # Making a non-ghost transfer.
        DUP ;
        PUSH @amount mutez 7 ;
        PUSH @ghost bool False ;
        TRANSFER_TOKENS ;
    } ;

    DIP {   # Making a ghost transfer.
        DUP ;
        PUSH @amount mutez 13 ;
        PUSH @ghost bool True ;
        TRANSFER_TOKENS ;
    } ;

    {   # Creating the list of all operations.
        DIP { DIP {NIL operation } ; CONS } ;
        CONS ;
    } ;

    APPLY_OPERATIONS ;

Finally, it checks that the balance and storage are the ones expected:

    GET_BALANCE ;
    PRINT_STACK ;
    STEP "retrieved the balance of the contract" ;

    PUSH mutez 23 ;
    IFCMPNEQ {
        PUSH string "balance should be 23utz" ;
        FAILWITH
    } {} ;

    GET_STORAGE nat ;

    IF_NONE {
        PUSH string "unable to retrieve storage of contract" ;
        FAILWITH
    } {
        PRINT_STACK ;
        STEP "retrieved the storage of the contract" ;
        PUSH nat 1 ;
        IFCMPNEQ {
            PUSH string "storage should be 1 (nat)" ;
            FAILWITH
        } {} ;
    }
}

The test passes and its output is

$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz -- rsc/simpleExample/okay/transfer.techel
Running test `Transfer`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation CREATE[uid:0] (@address[1]@main, "sha512:manager address", None, true, true, 3utz) "SimpleExample"
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: none
=> live contracts: SimpleExample (3utz) address[1]@main

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation TRANSFER[uid:1] address[0]@Transfer -> address[1]@main 7utz False
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: SimpleExample (3utz) address[1]@main

running TRANSFER[uid:1] address[0]@Transfer -> address[1]@main 7utz False
   timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: SimpleExample (10utz) address[1]@main

applying operation TRANSFER[uid:2] address[0]@Transfer -> address[1]@main 13utz True
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: SimpleExample (10utz) address[1]@main

running TRANSFER[uid:2] address[0]@Transfer -> address[1]@main 13utz True
   timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: SimpleExample (23utz) address[1]@main

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| address[1]@main                                                                                  |
| (contract bool)                                                                                  |
|--------------------------------------------------------------------------------------------------|
| 23utz                                                                                            |
| mutez                                                                                            |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [retrieved the balance of the contract] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| 1p                                                                                               |
| nat                                                                                              |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [retrieved the storage of the contract] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `Transfer`

Testing for Failures

This section is going to use a slightly more complex contract in order to showcase failures and how to test them. Even if you are not familiar with liquidity, the contract's code will most likely be more readable in liquidity than in michelson. Here is the liquidity version, admins.liq:

[%%version 0.405] 

type storage = {
  admins : (string, address) map ;
  (* Unused in this example. *)
  nus : (string, (address * tez * UnitContract.instance)) map ;
}

let admin_check (storage : storage) (name : string) (a : address) : unit =
  match Map.find name storage.admins with
  | None -> failwith "only admins can perform administrative tasks"
  | Some address ->
      if address <> a then
        failwith "illegal access to admin account"

let%entry add_admin
    ((admin_name, nu_name, nu_address) : string * string * address)
    (storage : storage)
  : operation list * storage 
=
  admin_check storage admin_name (Current.sender ()); 
  let storage =
    storage.admins <- Map.update nu_name (Some nu_address) storage.admins
  in
  [], storage

Note that the clients field of the storage is unused in this example. The admins map maps administrator names to addresses. The only entry point (in this example) is add_admin which allows administrators to add new administrators by registering their name and their address. More precisely, calling this contract is only legal if the SENDER (Current.sender ()) of the call is an administrator (c.f. admin_check). If the call to the contract is not legal, the transfer fails:

let admin_check (storage : storage) (name : string) (a : address) : unit =
  match Map.find name storage.admins with
  | None -> failwith "only admins can perform administrative tasks"
  | Some address ->
      if address <> a then
        failwith "illegal access to admin account"

The parameters of the entry point are

  • admin_name: name associated with the SENDER administrator,
  • nu_name: name of the new administrator to add,
  • nu_address: the address of the new administrator.
let%entry add_admin
    ((admin_name, nu_name, nu_address) : string * string * address)

Using liquidity to compile the contract to michelson (for instance using liquidity's online editor), we obtain admins.tz. Here are the storage and parameter types:

parameter (pair string (pair string address));
storage
  (pair :storage
     (map %admins string address)
     (map %clients string (pair address (pair mutez (contract :UnitContract unit)))));

We omit the contract's code (admins.tz) as i) it is not very readable and ii) we do not need to know what the code precisely is to create the contract and call it, as long as we know the storage and parameter types.

Creation

Creating a contract has been covered in previous sections, so let's give ourselves some code to create the contract with one administrator called root. In fact, let's make an account for root and register it as an administrator. The new administrator new_admin is also deployed as an account. Testcase create.techel does exactly that:

{
    NIL operation ;

    {   # Create an account for `root`.
        PUSH @balance mutez 0 ;
        PUSH @delegatable bool True ;
        PUSH @delegate (option key_hash) None ;
        PUSH @manager key "@root_manager" ;
        SHA512 ;
        CREATE_ACCOUNT @root ;
    } ;

    SWAP ;
    DIP { CONS } ;

    {   # Create an account for `new_admin`.
        PUSH @balance mutez 0 ;
        PUSH @delegatable bool True ;
        PUSH @delegate (option key_hash) None ;
        PUSH @manager key "@new_admin_manager" ;
        SHA512 ;
        CREATE_ACCOUNT @new_admin ;
    } ;

    SWAP ;
    DIP { SWAP ; DIP CONS } ;

    {   # Create an `admins` contract.

        # Create the storage's (empty) `clients` field.
        EMPTY_MAP @clients string (pair address (pair mutez (contract unit))) ;

        # Create the storage's `admins` field and register `root`.
        EMPTY_MAP @admins string address ;
        DUUUUP ; # Retrieve root's address.
        SOME @address ;
        PUSH @name string "root" ;
        PRINT_STACK ;
        UPDATE ;

        PAIR @storage ;
        PUSH @balance mutez 0 ;
        PUSH @delegatable bool True ;
        PUSH @spendable bool False ;
        PUSH @delegate (option key_hash) None ;
        PUSH @manager key "@contract_manager" ;
        SHA512 ;

        CREATE_CONTRACT @admins "Admins"
    } ;

    SWAP ;
    DIP { SWAP ; DIP { SWAP ; DIP CONS } } ;

    DIIIP { APPLY_OPERATIONS } ;

    PRINT_STACK ;
    STEP "after applying creation operations."
}

Running this test produces the following output

$ techelson --contract rsc/admins/contracts/admins.tz -- rsc/admins/okay/create.techel
Running test `Create`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| [ CREATE[uid:1] (@address[2]@new_admin, "sha512:@new_admin_manager", None, true, true, 0utz) 
      {
          storage unit ;
          parameter unit ;
          code ...;
      }, CREATE[uid:0] (@address[1]@root, "sha512:@root_manager", None, true, true, 0utz) 
             {
                 storage unit ;
                 parameter unit ;
                 code ...;
             } ] |
| (list operation)                                                                                 |
|--------------------------------------------------------------------------------------------------|
|                                                                                            @root |
| address[1]@root                                                                                  |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
|                                                                                       @new_admin |
| address[2]@new_admin                                                                             |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
|                                                                                         @clients |
| Map { }                                                                                          |
| (map string (pair address (pair mutez (contract unit))))                                         |
|--------------------------------------------------------------------------------------------------|
|                                                                                          @admins |
| Map { }                                                                                          |
| (map string address)                                                                             |
|--------------------------------------------------------------------------------------------------|
|                                                                                         @address |
| (Some address[1]@root)                                                                           |
| (option address)                                                                                 |
|--------------------------------------------------------------------------------------------------|
|                                                                                            @name |
| "root"                                                                                           |
| string                                                                                           |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation CREATE[uid:2] (@address[3]@admins, "sha512:@contract_manager", None, false, true, 0utz) "Admins"
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: none
=> live contracts: <anonymous> (0utz) address[2]@new_admin
                   Admins (0utz) address[3]@admins
                   <anonymous> (0utz) address[1]@root

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
|                                                                                            @root |
| address[1]@root                                                                                  |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
|                                                                                       @new_admin |
| address[2]@new_admin                                                                             |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
|                                                                                          @admins |
| address[3]@admins                                                                                |
| address                                                                                          |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [after applying creation operations.] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `Create`

Transfer Failures

Let's now add new_admin as a new administrator. Testcase addAdmin.techel only adds the following few instructions at the end of create.techel:

    # Retrieve the actual contract.
    CONTRACT (pair string (pair string address)) ;
    IF_NONE {
        PUSH string "failed to retrieve `admins` contract" ;
        STEP
    } {} ;

    # Saving the contract for later.
    DUP ;

    PUSH @amount mutez 0 ;

    # New admin's address.
    DUUUUP ;
    # New admin's name.
    PUSH @new_name string "new_admin" ;
    PAIR ;
    # Root's name.
    PUSH @name string "root" ;
    PAIR @storage ;

    TRANSFER_TOKENS ;

    DIP { NIL operation } ;
    CONS ;
    APPLY_OPERATIONS ;

What should the result of applying this transfer be? Remember than before adding an administrator, the contract checks that the sender is an admin.

let admin_check (storage : storage) (name : string) (a : address) : unit =
  match Map.find name storage.admins with
  | None -> failwith "only admins can perform administrative tasks"
  | Some address ->
      if address <> a then
        failwith "illegal access to admin account"

let%entry add_admin
    ((admin_name, nu_name, nu_address) : string * string * address)
...
=
  admin_check storage admin_name (Current.sender ()); 
...

So, if everything goes well, the transfer should fail: the sender here is not root, but the testcase. In techelson, the testcase currently running has its own address. It is in particular not the address of root. Hence, the transfer fails as it should and so does the whole testcase. The (relevant part of the) output is

Test `AddAdmin` failed:
    Error
        operation TRANSFER[uid:3] address[0]@AddAdmin -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)) was expected to succeed
        but failed on operation TRANSFER[uid:3] address[0]@AddAdmin -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
        operation failed on "illegal access to admin account" : string

You can see in the transfer the sender and the target of the transfer:

TRANSFER[uid:3] address[0]@AddAdmin -> address[3]@admins

AddAdmin is the name of our testcase, and address[0]@AddAdmin is its address. Name "root" does not map to this address in the contract and the transfer fails.

Handling Failures

Before getting into making this transfer work (next section), note that this (failing) testcase is actually useful. Or at least it should be: the transfer we are trying to make is illegal indeed. We do want the transfer to fail, but the testcase should

  • succeed if the transfer does fail,
  • fail if the transfer succeeds: anyone can add admins, which is bad.

This is what the MUST_FAIL techelson extension does. It takes an operation wraps it in a construct telling techelson that this operation must fail: either the operation itself or, if it is a transfer, the operations created by this transfer. Here is its signature:

instruction parameter stack
MUST_FAIL <type> :: option <type> : operation : 'S
-> operation : 'S

Let's ignore the <type> parameter and the first stack argument for now and just use this instruction right away. Testcase mustFail.techel is the same as addAdmin.techel except for a few lines after the transfer:

    TRANSFER_TOKENS ;
    PUSH (option string) None ;
    MUST_FAIL @this_must_fail string ;
    PRINT_STACK ;

The test now passes successfully:

$ techelson --contract rsc/admins/contracts/admins.tz -- rsc/admins/okay/mustFail.techel
Running test `MustFail`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation CREATE[uid:2] (@address[3]@admins, "sha512:@contract_manager", None, false, true, 0utz) "Admins"
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: none
=> live contracts: <anonymous> (0utz) address[2]@new_admin
                   Admins (0utz) address[3]@admins
                   <anonymous> (0utz) address[1]@root

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
|                                                                                            @root |
| address[1]@root                                                                                  |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
|                                                                                       @new_admin |
| address[2]@new_admin                                                                             |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
| address[3]@admins                                                                                |
| (contract (pair string (pair string address)))                                                   |
|--------------------------------------------------------------------------------------------------|
|                                                                                  @this_must_fail |
| MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))) |
| operation                                                                                        |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: <anonymous> (0utz) address[2]@new_admin
                   Admins (0utz) address[3]@admins
                   <anonymous> (0utz) address[1]@root

running TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
   timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@new_admin
                   Admins (0utz) address[3]@admins
                   <anonymous> (0utz) address[1]@root
failure confirmed on test operation
  MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
while running operation TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
failed with value "illegal access to admin account" : string

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `MustFail`

Notice that Techelson lets you know the failure is confirmed:

failure confirmed on test operation
  MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
while running operation TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
failed with value "illegal access to admin account" : string

(More) Precise Failure Testing

Now, MUST_FAIL (as it is used here) succeeds if the transfer ends in a tezos protocol failure. This include explicit failures in the code, illegal transfers due to insufficient funds, duplicate operations, etc. It does not include type-checking errors and internal techelson errors.

This means in particular that if the transfer above fails for a reason different from "illegal access to admin account" then MUST_FAIL will consider the test a success. To make sure the cause for failure is actually the one we want, we must use MUST_FAIL's optional stack parameter. A failure in michelson code always has a value of some type associated to it. In this case, the type of this value is string and its value is "illegal access to admin account". Testcase preciseMustFail.techel only changes mustFail.techel to pass the failure value expected to MUST_FAIL:

    TRANSFER_TOKENS ;
    PUSH (option string) (Some "illegal access to admin account") ;
    MUST_FAIL @this_must_fail string ;
    PRINT_STACK ;

As a consequence, if the transfer fails with anything else than an explicit failure with a value of type string equal to "illegal access to admin account", then the whole testcase will fail. Everything works fine here, and the output is

$ techelson --contract rsc/admins/contracts/admins.tz -- rsc/admins/okay/preciseMustFail.techel
Running test `PreciseMustFail`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation CREATE[uid:2] (@address[3]@admins, "sha512:@contract_manager", None, false, true, 0utz) "Admins"
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: none
=> live contracts: <anonymous> (0utz) address[2]@new_admin
                   Admins (0utz) address[3]@admins
                   <anonymous> (0utz) address[1]@root

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
|                                                                                            @root |
| address[1]@root                                                                                  |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
|                                                                                       @new_admin |
| address[2]@new_admin                                                                             |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
| address[3]@admins                                                                                |
| (contract (pair string (pair string address)))                                                   |
|--------------------------------------------------------------------------------------------------|
|                                                                                  @this_must_fail |
| MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))) |
| operation                                                                                        |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation MUST_FAIL[uid:4] "illegal access to admin account" : 
string (TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: <anonymous> (0utz) address[2]@new_admin
                   Admins (0utz) address[3]@admins
                   <anonymous> (0utz) address[1]@root

running TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
   timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@new_admin
                   Admins (0utz) address[3]@admins
                   <anonymous> (0utz) address[1]@root
failure confirmed on test operation
  MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
while running operation TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
failed with value "illegal access to admin account" : string

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `PreciseMustFail`

Notice that the MUST_FAIL operation now mentions the value expected:

MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] ...)

as opposed to the _ wildcard from testcase mustFail.techel, which means no value was given:

MUST_FAIL[uid:4] _  (TRANSFER[uid:3] ...)

Usurpation of Identity

The previous section used the liquidity contract admins.liq and its techelson version admins.tz. It showcased how to handle expected transfer failures and turn them into test objectives. The failure used to demonstrate the example was that we tried to add a new administrator by calling the contract from the testcase, which failed because only administrators can add other administrators, and the (address of the) testcase was not registered as such.

    PUSH @amount mutez 0 ;

    # New admin's address.
    DUUUUP ;
    # New admin's name.
    PUSH @new_name string "new_admin" ;
    PAIR ;
    # Root's name.
    PUSH @name string "root" ;
    PAIR @storage ;

    TRANSFER_TOKENS ;
    PUSH (option string) (Some "illegal access to admin account") ;
    MUST_FAIL @this_must_fail string ;
    PRINT_STACK ;

    DIP {NIL operation } ;
    CONS ;
    APPLY_OPERATIONS ;

One solution would be to register the testcase directly, but it would be more natural and more generic to be able to apply a transfer as someone else. Hence the SET_SOURCE extension:

instruction parameter stack
SET_SOURCE code :: address : 'A
-> 'B
iff code :: [ A -> B ]

Warning: the SET_SOURCE extension is only legal in testcases.

This extension allows to pretend the testcase is a live contract or account from the environment. More precisely, all operations created in the code under the SET_SOURCE will have their source and sender be the address from the stack. Testcase setSource.techel uses this instruction to pretend that root is the one adding a new administrator.

    # New admin's address.
    DUUUUP ;
    # New admin's name.
    PUSH @new_name string "new_admin" ;
    PAIR ;
    # Root's name.
    PUSH @name string "root" ;
    PAIR @storage ;

    {   # Pretending to be `root`.
        DUUUUUUP ;
        PRINT_STACK ;
        STEP ;
        SET_SOURCE {
            TRANSFER_TOKENS
        }
    } ;

    PRINT_STACK ;

    DIP { NIL operation } ;
    CONS ;

    APPLY_OPERATIONS

The testcase now succeeds, and its output is

$ techelson --contract rsc/admins/contracts/admins.tz -- rsc/admins/okay/setSource.techel
Running test `SetSource`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation CREATE[uid:2] (@address[3]@admins, "sha512:@contract_manager", None, false, true, 0utz) "Admins"
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: none
=> live contracts: <anonymous> (0utz) address[2]@new_admin
                   Admins (0utz) address[3]@admins
                   <anonymous> (0utz) address[1]@root

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
|                                                                                            @root |
| address[1]@root                                                                                  |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
|                                                                                       @new_admin |
| address[2]@new_admin                                                                             |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
| address[3]@admins                                                                                |
| (contract (pair string (pair string address)))                                                   |
|--------------------------------------------------------------------------------------------------|
| address[3]@admins                                                                                |
| (contract (pair string (pair string address)))                                                   |
|--------------------------------------------------------------------------------------------------|
|                                                                                          @amount |
| 0utz                                                                                             |
| mutez                                                                                            |
|--------------------------------------------------------------------------------------------------|
|                                                                                         @storage |
| ("root", ("new_admin", address[2]@new_admin))                                                    |
| (pair string (pair string address))                                                              |
|--------------------------------------------------------------------------------------------------|
| address[1]@root                                                                                  |
| address                                                                                          |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stopping [no information] press `return` to continue


running test script...
   timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
|                                                                                            @root |
| address[1]@root                                                                                  |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
|                                                                                       @new_admin |
| address[2]@new_admin                                                                             |
| address                                                                                          |
|--------------------------------------------------------------------------------------------------|
| address[3]@admins                                                                                |
| (contract (pair string (pair string address)))                                                   |
|--------------------------------------------------------------------------------------------------|
| TRANSFER[uid:3] address[1]@root -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)) |
| operation                                                                                        |
|==================================================================================================|

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation TRANSFER[uid:3] address[1]@root -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: <anonymous> (0utz) address[2]@new_admin
                   Admins (0utz) address[3]@admins
                   <anonymous> (0utz) address[1]@root

running TRANSFER[uid:3] address[1]@root -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
   timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@new_admin
                   Admins (0utz) address[3]@admins
                   <anonymous> (0utz) address[1]@root

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `SetSource`

Notice how, in the last PRINT_STACK, the sender of the transfer is now root:

TRANSFER[uid:3] address[1]@root -> address[3]@admins ...

Timestamp Control

Some contracts need to reason about time, based on the timestamp of the block the computation takes place in. By default, the timestamp of all blocks in Techelson is 1970-01-01T00:00:00Z. Testcases can set this timestamp to anything, the only constraint is that the new timestamp is older than the previous one. The relevant instruction is

instruction parameter stack
SET_TIMESTAMP none :: timestamp : 'S
-> 'S

As an example, consider the following contract Timestamp which takes (or unit (contract unit)): it

  • receives money once (Left Unit), and
  • unlocks it if one week has passed since it receives money.

To unlock the money, someone must call the contract and give it a unit contract to collect the money (Right <contract>). Unlocking the money is only legal after one week (604800 seconds) has passed since the money was received.

The code follows. Its storage is (option timestamp) which stores the last time it receives money. The contract fails if asked to

  • receive money but it's already storing money (its storage is not None),
  • unlock the money but it hasn't received anything (its storage is None), and
  • unlock the money but one week hasn't passed since it was received.
storage (option timestamp) ;
parameter (or unit (contract unit)) ;
code {
    UNPAIR @storage @param ;
    IF_LEFT {
        DROP ;
        IF_NONE {
            NOW ;
            SOME ;
            NIL operation ;
            PAIR
        } {
            PUSH string "cannot receive money twice" ;
            FAILWITH
        }
    } {
        SWAP ;
        IF_NONE {
            PUSH string "cannot send money, no money received" ;
            FAILWITH
        } {
            NOW ;
            SUB ;
            PUSH int 604800 ; # One week in seconds.
            IFCMPGT {
                PUSH string "cannot send money, it has not been one week yet" ;
                FAILWITH
            } {
                BALANCE ;
                UNIT ;
                TRANSFER_TOKENS ;
                DIP {
                    NONE timestamp ;
                    NIL operation
                } ;
                CONS ;
                PAIR
            }
        }
    } ;
} ;

Let's go through TestTimestamp, the testcase for Timestamp. The first step should be unsurprising by now: deploy the contract and an account (so that we can unlock the money later).

{
    { # Deploy contract.
        NONE timestamp ;
        PUSH mutez 0 ;
        PUSH bool False ;
        PUSH bool True ;
        NONE key_hash ;
        PUSH key "manager" ;
        HASH_KEY ;
        CREATE_CONTRACT @contract "Timestamp" ;
        DIP NIL operation ;
        CONS ;
    } ;

    { # Deploy account to retrieve the money later on.
        PUSH mutez 0 ;
        PUSH bool True ;
        NONE key_hash ;
        PUSH key "manager" ;
        HASH_KEY ;
        CREATE_ACCOUNT @account ;
        DIP SWAP
    } ;

    CONS ;
    APPLY_OPERATIONS ;
    SWAP ;

    { # Retrieve contract.
        CONTRACT (or unit (contract unit)) ;
        IF_NONE {
            PUSH string "failed to retrieve contract" ;
            FAILWITH
        } {}
    } ;

    DIP { # Retrieve account.
        CONTRACT unit ;
        IF_NONE {
            PUSH string "failed to retrieve account" ;
            FAILWITH
        } {}
    } ;

Next, let's set the timestamp to January 1 2019, 11am, and send some money to the contract.

    { # Set timestamp.
        PUSH timestamp "2019-01-01T11:00:00Z" ;
        SET_TIMESTAMP
    } ;

    { # Send money to the account.
        DUP ;
        PUSH mutez 10 ;
        UNIT ;
        LEFT (contract unit) ;
        TRANSFER_TOKENS ;
        DIP NIL operation ;
        CONS ;
        APPLY_OPERATIONS
    } ;

We now check the storage is what it should be:

    { # Check the storage is correct.
        DUP ;
        GET_STORAGE (option timestamp) ;
        IF_NONE {
            PUSH string "failed to retrieve storage" ;
            FAILWITH
        } {
            IF_NONE {
                PUSH string "storage should not be `None`" ;
                FAILWITH
            } {
                PUSH timestamp "2019-01-01T11:00:00Z" ;
                IFCMPNEQ {
                    PUSH string "storage should be `2019-01-01T11:00:00Z`" ;
                    FAILWITH
                } {}
            }
        }
    } ;

Let's make sure unlocking the money before one week has passed fails. First, the testcase sets the timestamp to January 8 2019, 9am, which is not one week later than the date at which we sent money to the contract. So this should fail.

    { # Set timestamp to almost one week later.
        PUSH timestamp "2019-01-08T09:00:00Z" ;
        SET_TIMESTAMP
    } ;

    { # Try to retrieve the money.
        DUP ;
        PUSH mutez 0 ;
        DUUUUP ;
        RIGHT unit ;
        TRANSFER_TOKENS ;
        PUSH (option string) (Some "cannot send money, it has not been one week yet") ;
        MUST_FAIL string ;
        DIP NIL operation ;
        CONS ;
        APPLY_OPERATIONS
    } ;

Last, let's set the date to January 8 2019, 11am, at which point unlocking the money should work.

    { # Set timestamp to exactly one week later.
        PUSH timestamp "2019-01-08T11:00:00Z" ;
        SET_TIMESTAMP
    } ;

    { # Try to retrieve the money.
        DUP ;
        PUSH mutez 0 ;
        DUUUUP ;
        RIGHT unit ;
        TRANSFER_TOKENS ;
        DIP NIL operation ;
        CONS ;
        APPLY_OPERATIONS
    } ;

    { # Check the account received it.
        DROP ;
        GET_BALANCE ;
        PUSH mutez 10 ;
        IFCMPNEQ {
            PUSH string "account's balance should be 10utz" ;
            FAILWITH
        } {}
    }
}

All set. Running techelson yields the following output. It is split in two parts here, first up to the request to unlock the money on January 8 2019 at 9am, which should fail:

$ techelson --contract rsc/timestamp/contracts/timestamp.tz -- rsc/timestamp/okay/testTimestamp.techel
Running test `TestTimestamp`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation CREATE[uid:1] (@address[2]@account, "b58check:manager", None, true, true, 0utz) 
                       {
                           storage unit ;
                           parameter unit ;
                           code ...;
                       }
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: none
=> live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (0utz) address[1]@contract

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation TRANSFER[uid:2] address[0]@TestTimestamp -> address[1]@contract 10utz (Left Unit)
   timestamp: 2019-01-01 11:00:00 +00:00
   live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (0utz) address[1]@contract

running TRANSFER[uid:2] address[0]@TestTimestamp -> address[1]@contract 10utz (Left Unit)
   timestamp: 2019-01-01 11:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (10utz) address[1]@contract

running test script...
   timestamp: 2019-01-01 11:00:00 +00:00

applying operation MUST_FAIL[uid:4] "cannot send money, it has not been one week yet" : 
string (TRANSFER[uid:3] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account))
   timestamp: 2019-01-08 09:00:00 +00:00
   live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (10utz) address[1]@contract

running TRANSFER[uid:3] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account)
   timestamp: 2019-01-08 09:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (10utz) address[1]@contract
failure confirmed on test operation
  MUST_FAIL[uid:4] "cannot send money, it has not been one week yet" : 
  string (TRANSFER[uid:3] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account))
while running operation TRANSFER[uid:3] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account)
failed with value "cannot send money, it has not been one week yet" : 
string

So far so good. Finally, the rest of the output should go smoothly and succeed:

running test script...
   timestamp: 2019-01-08 09:00:00 +00:00

applying operation TRANSFER[uid:5] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account)
   timestamp: 2019-01-08 11:00:00 +00:00
   live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (10utz) address[1]@contract

running TRANSFER[uid:5] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account)
   timestamp: 2019-01-08 11:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (10utz) address[1]@contract

applying operation TRANSFER[uid:6] address[1]@contract -> address[2]@account 10utz Unit
   timestamp: 2019-01-08 11:00:00 +00:00
   live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (10utz) address[1]@contract

running TRANSFER[uid:6] address[1]@contract -> address[2]@account 10utz Unit
   timestamp: 2019-01-08 11:00:00 +00:00
=> live contracts: <anonymous> (10utz) address[2]@account
                   Timestamp (0utz) address[1]@contract

running test script...
   timestamp: 2019-01-08 11:00:00 +00:00

Done running test `TestTimestamp`

Test Generation

Techelson has a test generation feature. It is relatively naive: the testcases it generates do not really take the semantics of your contract into account. Given a contract, it will generate a random storage for that contract and deploy it. Then, it will create a random number of transfers to that contract with random parameters.

Note that while test generation is random, it is expected to be deterministic: the same test generation command on a contract should always generate the same test cases.

It is naive in the sense that it can (and statistically will) generate testcases which are not successful. Still, this feature is useful to generate a testcase skeleton with random contract creation and transfers that you can edit to test the behavior of your contract.

Example

Let's showcase testgeneration on simpleExample.tz:

storage nat;
parameter bool;
code {
    UNPAIR;        # Unpair parameter and storage.
    IF {           # Asked not to count: storage is unchanged, nothing to do.
    } {
        PUSH nat 1;
        ADD
    };
    NIL operation; # We don't want to perform any operations.
    PAIR           # Aggregate the operation list and the new storage.
};

Test generation is activated by passing to techelson a testgen argument triggering the test generation mode. You can read more about modes in the Usage section. First, let's generate a single testcase (-n 1, or --count 1) and let techelson run it:

$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz testgen -n 1
done generating test for contract SimpleExample
Running test `SimpleExampleTest1`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation CREATE[uid:0] (@address[1], "blake2b:11a6ffbc9fb85", None, false, false, 638557938255190utz) "SimpleExample"
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: none
=> live contracts: SimpleExample (638557938255190utz) address[1]

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation TRANSFER[uid:1] address[0]@SimpleExampleTest1 -> address[1] 120584798270008utz True
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: SimpleExample (638557938255190utz) address[1]

running TRANSFER[uid:1] address[0]@SimpleExampleTest1 -> address[1] 120584798270008utz True
   timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: SimpleExample (759142736525198utz) address[1]

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `SimpleExampleTest1`

While this can be useful for simple contracts, usually you want to retrieve the testcase directly so that you can modify it to suit your needs. So let's still generate one testcase but this time we will dump it in the current directory (trailing . in the techelson command).

$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz testgen -n 1 . ; echo ; echo "testcase:" ; echo ; cat SimpleExampleTest1.techel ; rm SimpleExampleTest1.techel
done generating test for contract SimpleExample
dumping testcases to `.`

testcase:

{
  # deploying contract `SimpleExample`
  {
    # creating contract creation parameters
    {
      # creating storage for contract `SimpleExample`
      PUSH nat 807338681362247 ;
      # money transferred
      PUSH mutez 638557938255190 ;
      # delegatable
      PUSH bool False ;
      # spendable
      PUSH bool False ;
      # delegate
      NONE key_hash ;
      # manager
      {
        PUSH key "11a6ffbc9fb85" ;
        BLAKE2B
      }
    } ;
    CREATE_CONTRACT "SimpleExample"
  } ;
  # create a list of operations and apply
  {
    NIL operation ;
    SWAP ;
    CONS ;
    APPLY_OPERATIONS
  } ;
  # create transfer operation
  {
    DUP ;
    # retrieve contract from address, fail if none
    {
      CONTRACT bool ;
      IF_NONE {
                PUSH string "unable to spawn contract `SimpleExample`" ;
                FAILWITH
              }
              {}
    } ;
    PUSH mutez 120584798270008 ;
    PUSH bool True ;
    TRANSFER_TOKENS
  } ;
  # create a list of operations and apply
  {
    NIL operation ;
    SWAP ;
    CONS ;
    APPLY_OPERATIONS
  }
}

Quick Reference

This section provides a quick overview of some of techelson's features:

These sections try to provide relatively raw, brief and crucial information about the features they discuss. They will usually point to the relevant sections of this book if you need more details.

Extensions

Warning: the following instructions can only be used in contracts, not testcases:

  • SENDER
  • SOURCE

Techelson testcases have access to an extended instruction set to ease the process of writing tests. See rsc/tests/ for more examples. Note that techelson treats #> as whitespace, so you can use extensions in contracts (when legal) while keeping them pure michelson, like in the example below

DIP {
    ...
    CONS ;
    #> PRINT_STACK ;
    #> STEP "after list cons" ;
    PUSH int 3 ;
    ...
}

Unrestricted Extensions

The following instructions are legal in testcases and contracts:

  • STEP and STEP <string>:

    'S -> 'S

    Since 0.7.0, relevant section of this book: First Steps.

    suspends the evaluator and prints a string, if any.

  • PRINT_STACK:

    'S -> 'S

    Since 0.7.0, relevant section of this book: First Steps.

    prints the current state of the stack

Note that Techelson treats #> as whitespaces. You can have #> STEP "important step" ; in your contracts which keeps them pure Michelson, but Techelson will stop when it reaches the STEP.

Testcase-only Extensions

The following instructions are only legal in testcases:

  • CREATE_CONTRACT <string>:

    :: key_hash : option key_hash : bool : bool : mutez : 'g : 'S -> operation : address : 'S

    Since 0.7.0, relevant section of this book: Creating and Calling Contracts.

    Creates an instance of the contract named after the string parameter. The type of the storage of the contract must be 'g.

  • SPAWN_CONTRACT 'g:

    :: string : key_hash : option key_hash : bool : bool : mutez : 'g : 'S -> operation : address : 'S

    Since 0.7.0, relevant section of this book: Creating and Calling Contracts.

    Exactly the same as the CREATE_CONTRACT extension above but the name of the contract is passed as a stack parameter, and the type of the storage is an explicit parameter of the instruction.

  • APPLY_OPERATIONS:

    (list operation) : 'S -> 'S

    Since 0.7.0, relevant section of this book: Creating and Calling Contracts.

    • consumes a list of operations
    • suspends the execution of the testcase
    • applies all the operations in the list (these operations can create operations which will be applied too)
    • resumes the execution of the testcase
  • GET_STORAGE 'storage:

    contract _ : 'S -> (option 'storage) : 'S

    address : 'S -> (option 'storage) : 'S

    Since 0.7.0, relevant section of this book: Live Contract Inspection.

    • consumes a contract
    • pushes Some of the current value of the storage of the contract if its storage has type 'storage, None otherwise
  • GET_BALANCE:

    contract _ : 'S -> mutez : 'S

    address : 'S -> mutez : 'S

    Since 0.7.0, relevant section of this book: Live Contract Inspection.

    Same as GET_STORAGE, but pushes the balance of the contract instead of its storage

  • MUST_FAIL 'a:

    (option 'a) : operation : 'S -> 'S

    Since 0.7.0, relevant section of this book: Testing for Failures.

    Specifies that an operation (or the operation it creates recursively) must fail, and optionaly that it FAILedWITH a certain value. More precisely, the whole testcase will fail if the operation or the operations it creates recursively, when applied, either

    • succeeds, or
    • the (option 'a) parameter is (Some value) and the operation's failure was not caused by a FAILWITH on precisely value

    Note that if the optional value is NONE, then MUST_FAIL accepts any kind of protocol failure, not just FAILWITH. For instance, it will also accept creation/transfer operations that fail because of insufficient balance, because this precise operation already ran (it was DUP-ed), etc.

  • SET_SOURCE code, with code :: [ 'A -> 'B ]

    address :: 'A -> 'B

    Since 0.7.0, relevant section of this book: Usurpation of Identity.

    Sets the source of the testcase. Without this extension, the source of all transfers can only be the testcase. This allows to run code while pretending the testcase is a different live contract.

  • SET_TIMESTAMP

    timestamp :: 'S -> 'S

    Since 0.7.0, relevant section of this book: Timestamp Control.

    Sets the timestamp of the next block to some value. The default timestamp is 1970-01-01T00:00:00Z.

Command-Line Options

Warning: so-called contract initializers are mentioned in the --help but are not currently supported.

The full list of options (for the nominal mode) is obtained with --help:

$ techelson --help
techelson v0.7.0

USAGE:
    ../bin/techelson [OPTIONS] -- [FILES]*
    ../bin/techelson [OPTIONS] testgen [TESTGEN_OPTIONS] [-- DIR]?

OPTIONS:
    -h, --help
            prints this help message
    -v, --verb <int>?
            increases or sets verbosity [default: 1]
    -q
            decreases verbosity
    -s, --step (on|true|True|no|off|false|False)?
            (de)activates step-by-step evaluation [default: false]
    --skip (on|true|True|no|off|false|False)?
            if true, all steps will automatically advance (and `--step` will be set to
            false) [default: false]
    --contract <string> ',' <string>?
            adds a contract to the test environment. The second optional argument is the
            contract's initializer.

MODES:
    testgen
        activates and controls test generation
    run `../bin/techelson <MODE> --help` to obtain help on a specific mode. For example: `../bin/techelson testgen --help`

Modes

Modes available:

Techelson's modes are triggered by simply passing the name of the mode as an argument. For instance,

$ techelson --contract rsc/simpleExample/contracts/simpleExample.tz testgen -n 2
done generating tests for contract SimpleExample
Running test `SimpleExampleTest1`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation CREATE[uid:0] (@address[1], "blake2b:11a6ffbc9fb85", None, false, false, 638557938255190utz) "SimpleExample"
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: none
=> live contracts: SimpleExample (638557938255190utz) address[1]

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation TRANSFER[uid:1] address[0]@SimpleExampleTest1 -> address[1] 120584798270008utz True
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: SimpleExample (638557938255190utz) address[1]

running TRANSFER[uid:1] address[0]@SimpleExampleTest1 -> address[1] 120584798270008utz True
   timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: SimpleExample (759142736525198utz) address[1]

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `SimpleExampleTest1`
Running test `SimpleExampleTest2`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation CREATE[uid:0] (@address[3], "blake2b:", (Some "blake2b:"), true, true, 290035844265409utz) "SimpleExample"
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: none
=> live contracts: SimpleExample (290035844265409utz) address[3]

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation TRANSFER[uid:1] address[2]@SimpleExampleTest2 -> address[3] 520090578679187utz True
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: SimpleExample (290035844265409utz) address[3]

running TRANSFER[uid:1] address[2]@SimpleExampleTest2 -> address[3] 520090578679187utz True
   timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: SimpleExample (810126422944596utz) address[3]

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation TRANSFER[uid:2] address[2]@SimpleExampleTest2 -> address[3] 845034275504874utz False
   timestamp: 1970-01-01 00:00:00 +00:00
   live contracts: SimpleExample (810126422944596utz) address[3]

running TRANSFER[uid:2] address[2]@SimpleExampleTest2 -> address[3] 845034275504874utz False
   timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: SimpleExample (1655160698449470utz) address[3]

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

Done running test `SimpleExampleTest2`

You can have techelson print the options for mode <mode> with techelson <mode> --help.

Test Generation

Given a contract, this mode is in charge of generating a testcase automatically. The relevant chapter of this book is Test Generation.

$ techelson testgen --help
Generates testcases for some contract(s). If a directory is provided, the testcases will
be dumped there. Otherwise techelson will just run the testcases it generated.

USAGE:
    ../bin/techelson [OPTIONS] testgen [TESTGEN_OPTIONS] [-- DIR]?

TESTGEN_OPTIONS:
    -h, --help
            prints this help message
    -n, --count <int>
            sets the number of testcases to generate [default: 1]