Tutorial for scenarios in Factori

Published on 2022/11/29


While doing this tutorial, feel free to browse the documentation of the DSL features : Scenarios Documentation

TLDR

In this tutorial, we introduce a new scenario Domain Specific Language (DSL) embedded in OCaml, with the following features:

  • the DSL compiles down to OCaml, Typescript, and (soon) octez-client scripts;
  • the DSL has a fork feature enabling one to create separate universes with alternative scenarios. These scenarios then run in parallel, they live on the same blockchain, but the accounts and contracts are compartmentalized so that they might as well live on different ones;

There are various kinds of DSLs, so to disambiguate, this one is best described by this excerpt from Wikipedia:

"Embedded (or Internal) Domain Specific Languages are typically implemented within a host language as a library and tend to be limited to the syntax of the host language, though this depends on host language capabilities."

Don't hesitate to go to the troubleshooting section if something goes wrong.

Introduction

If you know anything about Functori, you may know that we like generating code. It's got multiple benefits, from automating tedious, error-prone boilerplate code to writing code for programming languages using a safer strongly-statically typed language. One benefit of this meta-programming is that once you've written a generator in one language, you can quickly write another one for another language. Yet another advantage is that once you have an intermediate AST (Abstract Syntax Tree), you can manipulate it at will and bend it to new uses you haven't considered yet. There's a bit of all this in today's post, which is developer-oriented.

If you remember the last tutorial, we squeezed every last bit of pedagogical value from Claude Barde's Rock Paper Scissors contract. Well, there's more! Rock Paper Scissors has historically been a very nice example of a smart contract that is simple enough that anyone can understand the stakes and hard enough that you'll probably get it wrong when you try to implement it the first few times.

Claude's particular instance relies on an on-chain generator, which we could argue against here if it didn't give us a perfect toy example to demonstrate our new scenario DSL.

Here are the rules of the game:

  • It costs 0.16tz to play;
  • You get some prize amount if you win;
  • You get the jackpot if you win three times in a row.

How could we go about testing this property? We have to set up a scenario where we play and win three times in a row. Unfortunately, that is only going to happen every 3^3 = 27 tries on average. Sure, we could try over and over until we succeed, but this would yield chaotic results, where some runs would succeed immediately while others would take dozens of trials. Ideally, we would like to make this reproducible (to some extent, given the whole pseudo-random generator situation, after all).

What if we could simulate all possible runs in parallel? This would give us:

  • reproducibility: every possible outcome would be represented every time we ran our simulation
  • speed: our simulation would only take the time it takes for one player to play three times in a row rather than 27 x that amount of time.

As you probably guessed, this is exactly what we're going to do. Alice is going to play Rock Paper Scissors three times in a row, and just before she plays, we're going to fork the universe into 3 universes, in which she plays Rock, Paper, and Scissors, respectively. In the end, we will have 27 universes where, collectively, all possible sequences of three plays will have been realized. Then we will compile these 27 scenarios to OCaml and/or Typescript, and run them in parallel on the flextesa sandbox.

You might wonder why we need to compile scenarios in so many languages. Indeed running (i.e., interpreting) them should be enough? Here are a few reasons why we do this:

  • Internally, we use OCaml, but we collaborate with others who work in Typescript, for example. If we're going to give them scenarios, they would rather have them in that language than have to install a whole setup in an unfamiliar programming language. With this, we get the best of both worlds: we write our scenarios in OCaml and give them a nice Typescript output.
  • complementing the previous point, producing scenarios deploying and calling contracts is a great way to get free examples of how to do this in the target language;
  • Having several target languages also enables us to cross-check that the tools we use to interact with the blockchain are consistent between them: this might help detect bugs in say, Taquito or our internal library Tzfunc;
  • Compiling to several languages is actually not that expensive engineering-wise, because OCaml is a wonderful language for compilers and transpilers; on top of that, we already have all the building blocks thanks to the Great Factori tool;
  • octez-client (formerly known as tezos-client) scenarios (which is not, as of now, a working feature) will be very portable: you can send them to anyone with a Tezos node/sandbox, and they can immediately run it;
  • We envision supporting more programming languages in the future, for which an SDK for interacting and signing with the Tezos blockchain is available (Python, C#, etc).

Setting up our environment

As usual, you're going to need factori for this one (both the tool and the library). Please refer to this blog post for example for installation.

In some working folder, do:

$ mkdir rockpaperscissors
$ cd rockpaperscissors

Here are the mainnet contracts we are going to examine and play with in this tutorial:

Let's import our two contracts

$ factori import kt1 . KT1DrZokUnBg35YANi5sQxGfyWgDSAJRfJqY --name rps --ocaml --typescript --web --force
$ factori import kt1 . KT1UcszCkuL5eMpErhWxpRmuniAecD227Dwp --name randomizer --ocaml --typescript --web --force

Let's create a folder for writing our (abstract) scenario:

$ mkdir src/ocaml_abstract_scenario

We need to have a local switch and dependencies (this may take a few minutes):

$ make _opam
$ make deps
$ opam pin add factori https://gitlab.com/functori/dev/factori.git
$ opam install factori

In src/ocaml_abstract_scenario/dune, we are going to declare an executable that is going to generate our scenarios in OCaml and TypeScript.

(executable
  (name compile_scenarios)
  (modules compile_scenarios)
(libraries abstract_scenarios)
)

(library
  (name abstract_scenarios)
  (modules abstract_scenarios)
  (libraries
     rps_ocaml_interface
     rps_abstract_ocaml_interface
     randomizer_ocaml_interface
     randomizer_abstract_ocaml_interface
     blockchain
     utils
     ez_api.icurl_lwt
     lwt.unix
     factori.factori_scenario_dsl)
  (preprocess (pps factori.ppx)))

Now we can get started on our actual scenario in file src/ocaml_abstract_scenario/abstract_scenarios.ml

Because we are using an embedded DSL, we have a few more constraints than we would if we directly wrote a scenario in OCaml or Typescript (which you already can, and if you don't need the advanced features of this DSL, you should probably stick to that!). For instance, we need to explicitly declare values before using them, such as the network, the deploying amount, etc. It might take some getting used to, but the scenarios provided in this tutorial will act as a reference, and we hope you'll see it's worth the effort.

Writing the scenario

Warmup

Let's warm up by writing a banal scenario that deploys randomizer, rps and plays Rock.

Let's open a few modules. The first two are the (abstract) interfaces to each of our two contracts. The second one is the generic interface for writing scenarios. The last one is for building values.

module RpsA = Rps_abstract_ocaml_interface
module RandomizerA = Randomizer_abstract_ocaml_interface
open Scenario_dsl.AstInterface
open Factori_abstract_types.Abstract

Let's define right away the function which will compile our scenarios, without commenting too much on it:

(* Compilation of scenario1 to OCaml/Typescript *)
let compile scenario scenario_name =
  let ocaml_output_file = "src/ocaml_scenarios/scenario.ml" in
  let typescript_output_file =
    Format.sprintf "src/ts-sdk/src/%s.ts" scenario_name
  in
  let ocaml_scenario =
    Format.asprintf
      "%a"
      (Scenario_main.ocaml_of_scenario ~funding:true ~emptying:true)
      scenario
  in
  let typescript_scenario =
    Format.asprintf
      "%a"
      (Scenario_main.typescript_of_scenario ~funding:true ~emptying:true)
      scenario
  in
  let _ = Factori_utils.write_file ocaml_output_file ocaml_scenario in
  let _ = Factori_utils.write_file typescript_output_file typescript_scenario in
  ()

Here's our first scenario. Notice how every primitive such as gen_agent, get_address, mk_network, etc... all need to know which scenario they are adding to.

let scenario1 () =
  let sc = new_scenario () in
  (* declare a new scenario *)
  let originator = gen_agent sc 0 in
  (* generate an agent (same number gives same agent) to originate *)
  let originator_address = get_address sc originator in
  (* get the address of the agent *)
  let alice = gen_agent sc 1 in
  (* another agent, Alice, to play with the contract *)
  let alice_address = get_address sc alice in
  let alice_balance = get_balance sc alice_address in
  (* Compute Alice's balance*)
  let _print_balance = print_balance sc alice_balance in
  (* print Alice's balance *)
  let network = mk_network sc ParameterNetwork in
  (* Use whatever network will be given as a parameter to the scenario *)
  let initial_storage_randomizer =
    RandomizerA.abstract_initial_blockchain_storage
  in
  (* let's use the randomizer's blockchain storage *)
  let jackpot = Z.of_string "500000000" in
  (* we make the jackpot intentionally big so that it will stand out *)
  let amount_deploy = mk_amount sc jackpot in
  (* This jackpot will be given when deploying *)
  let randomizer =
    RandomizerA.mk_deploy
      ~scenario:sc
      ~from:originator
      ~network
      ~storage:initial_storage_randomizer
      ~amount:amount_deploy
  in
  (* deploy randomizer contract *)
  let jackpot_factor = 2 in
  (* How many wins in a row gives you the jackpot? Let's say two for now *)
  let[@storage] initial_storage_rps =
    (* Most of the initial storage is copied from the chain, but notice that the randomizer is given using its "id". *)
    let open RpsA in
    {
      accrued_fees = Z.zero;
      admin = Id originator_address.id;
      jackpot;
      jackpot_factor = Z.of_string (string_of_int jackpot_factor);
      mvp = None;
      paused = false;
      play_fee = Z.of_string "160000";
      played_games = Z.of_string "0";
      players = Literal [];
      prize = Z.of_string "500000";
      randomizer_address = Id randomizer.id;
      randomizer_creator = "tz1UZZnrre9H7KzAufFVm7ubuJh5cCfjGwam";
    }
  in
  let rps =
    RpsA.mk_deploy
      ~scenario:sc
      ~from:originator
      ~network
      ~storage:initial_storage_rps
      ~amount:amount_deploy
  in
  (* Deploy the RPS contract *)
  let amount_play = mk_amount sc (Z.of_string "660000") in
  (* in mutez, the amount required to play *)
  let[@param] param = Z.one in
  let _play_rps =
    RpsA.mk_call_play
      ~msg:"Rock"
      ~scenario:sc
      ~from:alice
      ~amount:amount_play
      ~kt1:rps
      ~network
      ~param
      ()
  in
  (* Play Rock *)
  let alice_balance = get_balance sc alice_address in
  let _print_balance = print_balance ~msg:"final balance" sc alice_balance in
  (* Print Alice's final balance *)
  sc

If you need, take some time to explore this (heavily commented) scenario. As warned before, it is quite verbose and if we were not trying to do something more complicated, we could probably do with traditional factori scenarios, as demonstrated in our previous tutorials.

Fill the file src/ocaml_abstract_scenario/compile_scenarios.ml with

open Abstract_scenarios
let () = compile (scenario1 ()) "scenario1"

We can compile this scenario by running:

$ dune exec src/ocaml_abstract_scenario/compile_scenarios.exe

You may want to take a look at the generated code and convince yourself that it does what we told it to. Notice that before launching scenario, the main function empties accounts and then funds them again to assure reproducible conditions.

Let's start a sandbox with a block every five seconds:

$ docker pull oxheadalpha/flextesa:latest
$ docker run --rm --name my-sandbox --detach -p 20000:20000 -e flextesa_node_cors_origin='*' -e block_time=5 oxheadalpha/flextesa:latest kathmandubox start

If you like, you can look at the blockchain on explorus.functori.com/explorer (make sure you click on the settings button on the right and set the node to localhost:20000).

We can execute the OCaml scenario (if it doesn't work right away, wait for a few seconds, while the blockchain gets started):

$ make run_scenario_ocaml

Here is an example of successful output:

$ make run_scenario_ocaml
dune build
dune exec ./src/ocaml_scenarios/scenario.exe
[][balance] 5660000
[scenario]Entering scenario
[scenario]Deployed KT1: KT1DzEyvmoHoPvnk2FdNdF5wFRkCTht9h4n8
[scenario]Deployed KT1: KT1X5ahc8JrYMjcuNodB4nXHm6QfhD2B29TA
[][balance][final balance] 5567593

For the Typescript scenario, make sure you have npm installed and to run make ts-deps before running the following command:

$ make ts-deps
$ make ts
$ node src/ts-sdk/dist/scenario1.js

Here is an example of successful output:

$ node src/ts-sdk/dist/scenario1.js
Entering main
Finished funding. Launching scenarios
[scenario]Entering scenario
[][balance] 5659626
[scenario]Deployed KT1 KT1RoT6G3U97gZnygY9PA7g2wmLsiiWxtUXC
[scenario]Deployed KT1 KT1FwrqLUdgSNXni6u77AS7821XiE3aQaA6q
[][balance][final balance] 4976705

A more interesting scenario

Ok, now that we have set everything up, let us reap more sizeable rewards. In this next scenario, we "clone" Alice three times and make her play all three options Rock, Paper and Scissors in three parallel copies. Here it is:

let scenario2 () =
  let sc = new_scenario () in
  (* declare a new scenario *)
  let originator = gen_agent sc 0 in
  (* generate an agent (same number gives same agent) to originate *)
  let originator_address = get_address sc originator in
  (* get the address of the agent *)
  let alice = gen_agent sc 1 in
  (* another agent, Alice, to play with the contract *)
  let alice_address = get_address sc alice in
  let alice_balance = get_balance sc alice_address in
  (* Compute Alice's balance*)
  let _print_balance = print_balance sc alice_balance in
  (* print Alice's balance *)
  let network = mk_network sc ParameterNetwork in
  (* Use whatever network will be given as a parameter to the scenario *)
  let initial_storage_randomizer =
    RandomizerA.abstract_initial_blockchain_storage
  in
  (* let's use the randomizer's blockchain storage *)
  let jackpot = Z.of_string "500000000" in
  (* we make the jackpot intentionally big so that it will stand out *)
  let amount_deploy = mk_amount sc jackpot in
  (* This jackpot will be given when deploying *)
  let randomizer =
    RandomizerA.mk_deploy
      ~scenario:sc
      ~from:originator
      ~network
      ~storage:initial_storage_randomizer
      ~amount:amount_deploy
  in
  (* deploy randomizer contract *)
  let jackpot_factor = 2 in
  (* How many wins in a row gives you the jackpot? Let's say two for now *)
  let[@storage] initial_storage_rps =
    (* Most of the initial storage is copied from the chain, but notice that the randomizer is given using its "id". *)
    let open RpsA in
    {
      accrued_fees = Z.zero;
      admin = Id originator_address.id;
      jackpot;
      jackpot_factor = Z.of_string (string_of_int jackpot_factor);
      mvp = None;
      paused = false;
      play_fee = Z.of_string "160000";
      played_games = Z.of_string "0";
      players = Literal [];
      prize = Z.of_string "500000";
      randomizer_address = Id randomizer.id;
      randomizer_creator = "tz1UZZnrre9H7KzAufFVm7ubuJh5cCfjGwam";
    }
  in
  let rps =
    RpsA.mk_deploy
      ~scenario:sc
      ~from:originator
      ~network
      ~storage:initial_storage_rps
      ~amount:amount_deploy
  in
  (* Deploy the RPS contract *)
  let amount_play = mk_amount sc (Z.of_string "660000") in
  (* in mutez, the amount required to play *)
  (* Let's fork into the three possible options *)
  let fork_id = open_fork sc in
  let branch1 = add_branch fork_id in
  let[@param] param = Z.of_int 1 in
  let _play_rps =
    RpsA.mk_call_play
      ~msg:"Rock"
      ~scenario:branch1
      ~from:alice
      ~amount:amount_play
      ~kt1:rps
      ~network
      ~param
      ()
  in
  let alice_balance = get_balance branch1 alice_address in
  let _print_balance =
    print_balance ~msg:"final balance" branch1 alice_balance
  in
  (* Print Alice's final balance *)
  let branch2 = add_branch fork_id in
  let[@param] param = Z.of_int 2 in
  let _play_rps =
    RpsA.mk_call_play
      ~msg:"Paper"
      ~scenario:branch2
      ~from:alice
      ~amount:amount_play
      ~kt1:rps
      ~network
      ~param
      ()
  in
  let alice_balance = get_balance branch2 alice_address in
  let _print_balance =
    print_balance ~msg:"final balance" branch2 alice_balance
  in
  (* Print Alice's final balance *)
  let branch3 = add_branch fork_id in
  let[@param] param = Z.of_int 3 in
  let _play_rps =
    RpsA.mk_call_play
      ~msg:"Scissors"
      ~scenario:branch3
      ~from:alice
      ~amount:amount_play
      ~kt1:rps
      ~network
      ~param
      ()
  in
  let alice_balance = get_balance branch3 alice_address in
  let _print_balance =
    print_balance ~msg:"final balance" branch3 alice_balance
  in
  (* Print Alice's final balance *)
  let () = close_fork fork_id in
  sc

Change the file src/ocaml_abstract_scenario/compile_scenarios.ml to

open Abstract_scenarios
let () = compile (scenario2 ()) "scenario2"

and repeat the steps above:

$ make run_scenario_ocaml
dune build
dune exec ./src/ocaml_scenarios/scenario.exe
[2][balance] 5660000
[1][balance] 5660000
[0][balance] 5660000
[scenario2]Entering scenario2
[scenario1]Entering scenario1
[scenario0]Entering scenario0
[scenario2]Deployed KT1: KT1JB3kZuKpfK4Xst1JZxWGUCGaVfBxAsZCr
[scenario1]Deployed KT1: KT1RPiQx4Pn54Vrj7GTbCF6qB7hTqzmRgn6K
[scenario0]Deployed KT1: KT1SswrbDt13zH62ZukMKreY1pLxZHdGGg2W
[scenario2]Deployed KT1: KT1CmejVTn1XafUW9w5RC7Z1BnNmmGfS1UXJ
[scenario1]Deployed KT1: KT1WyFWoeNCsJhcC3ANLPMEepWhfMXfkFQtQ
[scenario0]Deployed KT1: KT1RuHcUFaTunycazyvSWoQscnqn4v2zWXvA
[2][balance][final balance] 4975956
[1][balance][final balance] 5567596
[0][balance][final balance] 4975956

As expected, Alice won once and lost twice, there's one version of her that is happy. Well, actually.. even when she wins she has less money than she started with. Is it to say that Rock Paper Scissors is a scam? No, it's just that the jackpot is reserved for people who win three times in a row. Which is why in the next step, we're going to make Alice play all possible choices... n times in a row.

Let's make it recursive

Our third scenario is prefaced with a function tasked with iterating the forking process we had at the end of the previous scenario. This function, called play_all_three_options_n_times takes among other things a scenario id sc and an integer n. Here is what it does:

  • If n=0, it does nothing: the recursion is over
  • if n >= 1: it forks sc into three branches branch1, branch2 and branch3
  • For each of these branches it calls play_all_three_options_n_times recursively with n-1.

The function play_all_three_options_n_times is called once by the main scenario3 function after deploying randomizer and rps.

In total, 3^n branches will be created. Note that if n=1, the third scenario is basically the same as the second.

(* Recursively fork between Rock, Paper and Scissors three times *)
let rec play_all_three_options_n_times rps network sc n =
  let alice = gen_agent sc 1 in
  let alice_address = get_address sc alice in
  if n <= 0 then sc
  else
    let assert_success = false in
    let msg = if n = 1 then "final" else "" in
    let amount_play = mk_amount sc (Z.of_string "660000") in
    let fork_id = open_fork sc in
    let branch1 = add_branch fork_id in
    let[@param] param = Z.one in
    let _play_rps =
      RpsA.mk_call_play
        ~assert_success
        ~msg:"Rock"
        ~scenario:branch1
        ~from:alice
        ~amount:amount_play
        ~kt1:rps
        ~network
        ~param
        ()
    in
    let alice_balance = get_balance branch1 alice_address in
    let _print_balance = print_balance ~msg branch1 alice_balance in
    let _ = play_all_three_options_n_times rps network branch1 (n - 1) in
    let branch2 = add_branch fork_id in
    let[@param] param = Z.of_int 2 in
    let _play_rps =
      RpsA.mk_call_play
        ~assert_success
        ~msg:"Paper"
        ~scenario:branch2
        ~from:alice
        ~amount:amount_play
        ~kt1:rps
        ~network
        ~param
        ()
    in
    let alice_balance = get_balance branch2 alice_address in
    let _print_balance = print_balance ~msg branch2 alice_balance in
    let _ = play_all_three_options_n_times rps network branch2 (n - 1) in
    let branch3 = add_branch fork_id in
    let[@param] param = Z.of_int 3 in
    let _play_rps =
      RpsA.mk_call_play
        ~assert_success
        ~msg:"Scissors"
        ~scenario:branch3
        ~from:alice
        ~amount:amount_play
        ~kt1:rps
        ~network
        ~param
        ()
    in
    let alice_balance = get_balance branch3 alice_address in
    let _print_balance = print_balance ~msg branch3 alice_balance in
    let _ = play_all_three_options_n_times rps network branch3 (n - 1) in
    let () = close_fork fork_id in
    sc

let scenario3 () =
  let sc = new_scenario () in
  let originator = gen_agent sc 0 in
  let originator_address = get_address sc originator in
  let alice = gen_agent sc 1 in
  let alice_address = get_address sc alice in
  let alice_balance = get_balance sc alice_address in
  let _print_balance = print_balance sc alice_balance in
  let network = mk_network sc ParameterNetwork in
  let initial_storage_randomizer =
    RandomizerA.abstract_initial_blockchain_storage
  in
  let jackpot = Z.of_string "500000000" in
  let amount_deploy = mk_amount sc jackpot in
  let randomizer =
    RandomizerA.mk_deploy
      ~scenario:sc
      ~from:originator
      ~network
      ~storage:initial_storage_randomizer
      ~amount:amount_deploy
  in
  let jackpot_factor = 2 in
  let[@storage] initial_storage_rps =
    let open RpsA in
    {
      accrued_fees = Z.zero;
      admin = Id originator_address.id;
      jackpot;
      jackpot_factor = Z.of_string (string_of_int jackpot_factor);
      mvp = None;
      paused = false;
      play_fee = Z.of_string "160000";
      played_games = Z.of_string "0";
      players = Literal [];
      prize = Z.of_string "500000";
      randomizer_address = Id randomizer.id;
      randomizer_creator = "tz1UZZnrre9H7KzAufFVm7ubuJh5cCfjGwam";
    }
  in
  let rps =
    RpsA.mk_deploy
      ~scenario:sc
      ~from:originator
      ~network
      ~storage:initial_storage_rps
      ~amount:amount_deploy
  in
  let _balance = get_balance sc originator_address in
  let _storage = get_storage sc rps in

  let _ = play_all_three_options_n_times rps network sc jackpot_factor in
  sc

As we have chosen n=jackpot_factor=2, this will generate 3^2=9 possible worlds with all possible outcomes (Rock Rock, Rock Paper, Rock Scissors, Scissors Rock, etc...).

Change the file src/ocaml_abstract_scenario/compile_scenarios.ml to

open Abstract_scenarios
let () = compile (scenario3 ()) "scenario3"

and repeat the steps above. You should see one version of Alice stand out with a bigger balance!

[2_2][balance][final] 21886915
[2_1][balance][final] 521886804
[2_0][balance][final] 21886915
[1_2][balance][final] 21295276
[1_1][balance][final] 21886916
[1_0][balance][final] 21295276
[0_2][balance][final] 21295276
[0_1][balance][final] 21886916
[0_0][balance][final] 21295276

Alice in the universe [2_1] won two times and she did get the jackpot!

Of course, you can change the value of jackpot_factor to 3 or more, but keep in mind that this will generate 3^n scenarios for jackpot_factor = n. (n=3, with 27 scenarios, will only work in OCaml)

Some things that might go wrong : Troubleshooting

Running many scenarios in parallel on a flextesa sandbox is unfortunately not an exact science, and you may encounter errors or weird behaviors. Here's how to deal with some of them:

  • If you get a "NO_RESULT_FROM_VIEW" error, restart the scenario.
  • If you run the Typescript scenario with too many scenarios in parallel, the flextesa sandbox might get stuck, and in that case you will need to restart the sandbox.
  • Finally, if you get a empty_transaction error while fiddling with the parameters of scenario 3, congratulations, you've found a bug in the rock paper scissors contract! We have found it and reported it. Note that this bug, if not caught during a code review, is really hard to find with the normal way of testing, because of how unlikely it is to get to that state by chance. We believe this is a novelty contribution to the testing tools on Tezos.

What's happening under the hood?

When compiling a scenario, Factori automatically does a lot of the tedious work (reveal, empty and fund all generated addresses). When you run it, all universes are run in parallel and their output appears on the standard output, prefixed by the index of the universe. In general, if there aren't too many scenarios (the limit is different in OCaml and Typescript), similar operations from different universes will occur in the same block (which is handy for this tutorial, because the random generator gives the same value to all Rock Paper Scissors instances, so that Alice plays the exact same adversary in all parallel universes).

Conclusion

Feel free to play with the provided scenarios or to invent some of your own! It's never been easier to make tests in parallel on a Tezos blockchain in two different programming languages. We are aiming to make this feature available for octez-client as well, so that we can make maximally portable scenarios.

By giving some abstraction to our scenarios, we are able to make tests that would have been cumbersome to write directly, and to execute them fast in parallel, in two different programming languages! A lot of work can still be done to improve the DSL and its features, and we have some exciting ideas for the future, but as you can see, it is already working and delivering results we believe no currently existing tool can provide.

Image placeholder