Introduction

Contract Lab (clsdk) is a set of tools and libraries for building, testing, and debugging smart contracts for Mandel. It has the following capabilities:

  • Compile contracts for Mandel 3.0, eosio 2.0, and earlier
  • Debug contracts using the debug_plugin, which comes built in to Contract Lab's nodeos executable
  • Create deterministic tests using cltester, which comes with Contract Lab
  • Debug contracts and tests using cltester
  • Bootstrap test chains using cltester and spawn a nodeos producer on them
  • Fork public chains into test chains

Navigating this Documentation

If the Table of Contents is not currently visible, then click the hamburger menu.

Ubuntu 20.04 Installation

This installs several dependencies then downloads and extracts both wasi-sdk and Contract Lab. wasi-sdk provides clang and other tools and provides the C and C++ runtime libraries built for WASM. Contract Lab provides libraries and tools for working with eosio.

For convenience, consider adding the environment variables below to ~/.bashrc or whatever is appropriate for the shell you use.

sudo apt-get update
sudo apt-get install -yq    \
    binaryen                \
    build-essential         \
    cmake                   \
    gdb                     \
    git                     \
    libboost-all-dev        \
    libcurl4-openssl-dev    \
    libgmp-dev              \
    libssl-dev              \
    libusb-1.0-0-dev        \
    pkg-config              \
    wget

export WASI_SDK_PREFIX=~/work/wasi-sdk-12.0
export CLSDK_PREFIX=~/work/clsdk

export PATH=$CLSDK_PREFIX/bin:$PATH

cd ~/work
wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-12/wasi-sdk-12.0-linux.tar.gz
tar xf wasi-sdk-12.0-linux.tar.gz

cd ~/work
wget https://github.com/gofractally/contract-lab/releases/download/v1.0.0-rc1/clsdk-ubuntu-20-04.tar.gz
tar xf clsdk-ubuntu-20-04.tar.gz

MacOS Installation (Monterey 12.1)

This installs several dependencies then downloads and extracts both wasi-sdk and Contract Lab. wasi-sdk provides clang and other tools and provides the C and C++ runtime libraries built for WASM. Contract Lab provides libraries and tools for working with eosio.

For convenience, consider adding the environment variables below to ~/.zshrc or whatever is appropriate for the shell you use.

brew install   \
    binaryen   \
    cmake      \
    gdb        \
    git        \
    boost      \
    lightgbm   \
    mpfi       \
    nss        \
    openssl@1.1 \
    pkg-config \
    wget       \
    zstd
export WASI_SDK_PREFIX=~/work/wasi-sdk-12.0
export CLSDK_PREFIX=~/work/clsdk
export PATH=$CLSDK_PREFIX/bin:$PATH

// A directory work--that can live anywhere--will hold a few 3rd party deps

cd ~/work
wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-12/wasi-sdk-12.0-macos.tar.gz
tar xf wasi-sdk-12.0-macos.tar.gz

cd ~/work
wget https://github.com/gofractally/contract-lab/releases/download/v1.0.0-rc1/clsdk-macos.tar.gz
tar xf clsdk-macos.tar.gz

Basic Contract

Here is a basic contract definition. Place example.cpp and CMakeLists.txt in an empty folder.

example.cpp

#include <eosio/asset.hpp>
#include <eosio/eosio.hpp>

// The contract class must be in a namespace
namespace example
{
   // The contract
   struct example_contract : public eosio::contract
   {
      // Use the base class constructors
      using eosio::contract::contract;

      // Action: user buys a dog
      void buydog(eosio::name user, eosio::name dog, const eosio::asset& price)
      {
         // TODO: buy a dog
      }
   };

   // First part of the dispatcher
   EOSIO_ACTIONS(example_contract,  //
                 "example"_n,       //
                 action(buydog, user, dog, price))
}  // namespace example

// Final part of the dispatcher
EOSIO_ACTION_DISPATCHER(example::actions)

// ABI generation
EOSIO_ABIGEN(actions(example::actions))

CMakeLists.txt

# All cmake projects need these
cmake_minimum_required(VERSION 3.16)
project(example)

# clsdk requires C++20
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Libraries for building contracts and tests
find_package(clsdk REQUIRED)

# Build example.wasm contract
add_executable(example example.cpp)
target_link_libraries(example eosio-contract-simple-malloc)

# Generate example.abi
# This is a 2-step process:
#   * Build example.abi.wasm. This must link to eosio-contract-abigen.
#   * Run the wasm to generate the abi
add_executable(example-abigen example.cpp)
target_link_libraries(example-abigen eosio-contract-abigen)
add_custom_command(TARGET example-abigen POST_BUILD
    COMMAND cltester example-abigen.wasm >example.abi
)

# These symlinks help vscode
execute_process(COMMAND ln -sf ${clsdk_DIR} ${CMAKE_CURRENT_BINARY_DIR}/clsdk)
execute_process(COMMAND ln -sf ${WASI_SDK_PREFIX} ${CMAKE_CURRENT_BINARY_DIR}/wasi-sdk)

# Generate compile_commands.json to aid vscode and other editors
set(CMAKE_EXPORT_COMPILE_COMMANDS on)

Building

This will create example.wasm and example.abi:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)

Trying the contract

clsdk comes with nodeos, cleos, and keosd. The following will execute the contract:

# Start keosd on an empty directory
killall keosd
rm -rf testing-wallet testing-wallet-password
mkdir testing-wallet
keosd --wallet-dir `pwd`/testing-wallet --unlock-timeout 99999999 >keosd.log 2>&1 &

# Create a default wallet. This saves the password in testing-wallet-password
cleos wallet create -f testing-wallet-password

# Add the default development key
cleos wallet import --private-key 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3

# Start up a fresh chain
killall nodeos
rm -rf data config
nodeos -d data --config-dir config --plugin eosio::chain_api_plugin --plugin eosio::producer_api_plugin -e -p eosio >nodeos.log 2>&1 &

# Install the contract
cleos create account eosio example EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi example example.abi
cleos set code example example.wasm

# Try out the contract (does nothing)
cleos push action example buydog '["eosio", "fido", "100.0000 EOS"]' -p eosio

vscode support

The following files configure vscode:

Code completion and symbol lookup does not work until the project is built (above).

Tables

Here is a contract that uses a table. Place table.cpp and CMakeLists.txt in an empty folder.

table.cpp

#include <eosio/asset.hpp>
#include <eosio/eosio.hpp>

namespace example
{
   // A purchased animal
   struct animal
   {
      eosio::name name;             // Name of animal
      eosio::name type;             // Type of animal
      eosio::name owner;            // Who owns the animal
      eosio::asset purchase_price;  // How much the owner paid

      uint64_t primary_key() const { return name.value; }
   };

   // This does 2 things:
   // * Controls which fields are stored in the table
   // * Lets the ABI generator know the field names
   EOSIO_REFLECT(animal, name, type, owner, purchase_price)

   // Table definition
   typedef eosio::multi_index<"animal"_n, animal> animal_table;

   struct example_contract : public eosio::contract
   {
      using eosio::contract::contract;

      // Action: user buys a dog
      void buydog(eosio::name user, eosio::name dog, const eosio::asset& price)
      {
         require_auth(user);
         animal_table table{get_self(), get_self().value};
         table.emplace(user, [&](auto& record) {
            record.name = dog;
            record.type = "dog"_n;
            record.owner = user;
            record.purchase_price = price;
         });
      }
   };

   EOSIO_ACTIONS(example_contract,  //
                 "example"_n,       //
                 action(buydog, user, dog, price))
}  // namespace example

EOSIO_ACTION_DISPATCHER(example::actions)

EOSIO_ABIGEN(
    // Include the contract actions in the ABI
    actions(example::actions),

    // Include the table in the ABI
    table("animal"_n, example::animal))

Additional files

Building

This will create table.wasm and table.abi:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)

Trying the contract

# Create some users
cleos create account eosio alice EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos create account eosio bob EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV

# Install the contract
cleos create account eosio table EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi table table.abi
cleos set code table table.wasm

# Try out the contract
cleos push action table buydog '["alice", "fido", "100.0000 EOS"]' -p alice
cleos push action table buydog '["alice", "rex", "120.0000 EOS"]' -p alice
cleos push action table buydog '["bob", "lambo", "70.0000 EOS"]' -p bob

# See the purchased animals
cleos get table table table animal

Notifications

This contract adds the following capabilities to the previous examples:

  • Receives notifications from eosio.token and tracks user balances
  • Deducts from the user balance whenever the user buys a dog

This example does not cover:

  • Removing empty balance records
  • Returning excess funds to users
  • Protecting against dust attacks on the balance table
  • Treating incoming funds from system accounts as special (e.g. unstaking, selling rex, selling ram)

Place notify.cpp and CMakeLists.txt in an empty folder.

notify.cpp

#include <eosio/asset.hpp>
#include <eosio/eosio.hpp>

namespace example
{
   // Keep track of deposited funds
   struct balance
   {
      eosio::name owner;
      eosio::asset balance;

      uint64_t primary_key() const { return owner.value; }
   };
   EOSIO_REFLECT(balance, owner, balance)
   typedef eosio::multi_index<"balance"_n, balance> balance_table;

   // A purchased animal
   struct animal
   {
      eosio::name name;
      eosio::name type;
      eosio::name owner;
      eosio::asset purchase_price;

      uint64_t primary_key() const { return name.value; }
   };
   EOSIO_REFLECT(animal, name, type, owner, purchase_price)
   typedef eosio::multi_index<"animal"_n, animal> animal_table;

   struct example_contract : public eosio::contract
   {
      using eosio::contract::contract;

      // eosio.token transfer notification
      void notify_transfer(eosio::name from,
                           eosio::name to,
                           const eosio::asset& quantity,
                           std::string memo)
      {
         // Only track incoming transfers
         if (from == get_self())
            return;

         // The dispatcher has already checked the token contract.
         // We need to check the token type.
         eosio::check(quantity.symbol == eosio::symbol{"EOS", 4},
                      "This contract does not deal with this token");

         // Record the change
         add_balance(from, quantity);
      }

      // Action: user buys a dog
      void buydog(eosio::name user, eosio::name dog, const eosio::asset& price)
      {
         require_auth(user);
         eosio::check(price.symbol == eosio::symbol{"EOS", 4},
                      "This contract does not deal with this token");
         eosio::check(price.amount >= 50'0000, "Dogs cost more than that");
         sub_balance(user, price);
         animal_table table{get_self(), get_self().value};
         table.emplace(user, [&](auto& record) {
            record.name = dog;
            record.type = "dog"_n;
            record.owner = user;
            record.purchase_price = price;
         });
      }

      // This is not an action; it's a function internal to the contract
      void add_balance(eosio::name owner, const eosio::asset& quantity)
      {
         balance_table table(get_self(), get_self().value);
         auto record = table.find(owner.value);
         if (record == table.end())
            table.emplace(get_self(), [&](auto& a) {
               a.owner = owner;
               a.balance = quantity;
            });
         else
            table.modify(record, eosio::same_payer, [&](auto& a) { a.balance += quantity; });
      }

      // This is not an action; it's a function internal to the contract
      void sub_balance(eosio::name owner, const eosio::asset& quantity)
      {
         balance_table table(get_self(), get_self().value);
         const auto& record = table.get(owner.value, "user does not have a balance");
         eosio::check(record.balance.amount >= quantity.amount, "not enough funds deposited");
         table.modify(record, owner, [&](auto& a) { a.balance -= quantity; });
      }
   };

   EOSIO_ACTIONS(example_contract,
                 "example"_n,
                 notify("eosio.token"_n, transfer),  // Hook up notification
                 action(buydog, user, dog, price))
}  // namespace example

EOSIO_ACTION_DISPATCHER(example::actions)

EOSIO_ABIGEN(actions(example::actions),
             table("balance"_n, example::balance),
             table("animal"_n, example::animal))

Additional files

Building

This will create notify.wasm and notify.abi:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)

Trying the contract

# Create some users
cleos create account eosio alice EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos create account eosio bob EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV

# Set up eosio.token
# Note: the build system created a symlink to clsdk for easy access to the token contract
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi eosio.token clsdk/contracts/token.abi
cleos set code eosio.token clsdk/contracts/token.wasm

cleos push action eosio.token create '["eosio", "1000000000.0000 EOS"]' -p eosio.token
cleos push action eosio.token issue '["eosio", "1000000000.0000 EOS", ""]' -p eosio
cleos push action eosio.token open '["alice", "4,EOS", "alice"]' -p alice
cleos push action eosio.token open '["bob", "4,EOS", "bob"]' -p bob
cleos push action eosio.token transfer '["eosio", "alice", "10000.0000 EOS", "have some"]' -p eosio
cleos push action eosio.token transfer '["eosio", "bob", "10000.0000 EOS", "have some"]' -p eosio

# Install the contract
cleos create account eosio notify EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi notify notify.abi
cleos set code notify notify.wasm

# Try out the contract
cleos push action eosio.token transfer '["alice", "notify", "300.0000 EOS", "for purchases"]' -p alice
cleos push action eosio.token transfer '["bob", "notify", "300.0000 EOS", "for purchases"]' -p bob

cleos push action notify buydog '["alice", "fido", "100.0000 EOS"]' -p alice
cleos push action notify buydog '["alice", "rex", "120.0000 EOS"]' -p alice
cleos push action notify buydog '["bob", "lambo", "70.0000 EOS"]' -p bob

# See the remaining balances and the purchased animals
cleos get table notify notify balance
cleos get table notify notify animal

Debugging

This contract is identical to the one in Notifications, except it extends CMakeLists.txt to build notify-debug.wasm and it has an additional config file (launch.json).

CMakeLists.txt

# All cmake projects need these
cmake_minimum_required(VERSION 3.16)
project(notify)

# clsdk requires C++20
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Libraries for building contracts and tests
find_package(clsdk REQUIRED)

# Build notify.wasm contract
add_executable(notify notify.cpp)
target_link_libraries(notify eosio-contract-simple-malloc)

# Build notify-debug.wasm
# This is like notify.wasm, but includes debugging information.
add_executable(notify-debug notify.cpp)
target_link_libraries(notify-debug eosio-contract-simple-malloc-debug)

# Generate notify.abi
# This is a 2-step process:
#   * Build notify.abi.wasm. This must link to eosio-contract-abigen.
#   * Run the wasm to generate the abi
add_executable(notify-abigen notify.cpp)
target_link_libraries(notify-abigen eosio-contract-abigen)
add_custom_command(TARGET notify-abigen POST_BUILD
    COMMAND cltester notify-abigen.wasm >notify.abi
)

# These symlinks help vscode
execute_process(COMMAND ln -sf ${clsdk_DIR} ${CMAKE_CURRENT_BINARY_DIR}/clsdk)
execute_process(COMMAND ln -sf ${WASI_SDK_PREFIX} ${CMAKE_CURRENT_BINARY_DIR}/wasi-sdk)

# Generate compile_commands.json to aid vscode and other editors
set(CMAKE_EXPORT_COMPILE_COMMANDS on)

Additional files

Building

This will create notify.wasm, notify-debug.wasm, and notify.abi:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)

Nodeos debug_plugin

debug_plugin (included in the nodeos binary that comes with clsdk) adds these new capabilities to nodeos:

  • Wasm substitution (--subst contract.wasm:debug.wasm). This instructs nodeos to execute debug.wasm whenever it would otherwise execute contract.wasm. nodeos identifies wasms by hash, so this affects all accounts which have the same wasm installed.
  • Relaxed wasm limits. Debugging wasms are usually much larger than normal contract wasms. debug_plugin removes eosio wasm limits to allow the larger wasms to execute. They are also slower, so it also removes execution time limits.
  • Debug info support. It transforms wasm debug info into native debug info. This enables gdb to debug executing contracts.

Only substituted wasms get the relaxed limits and debug info support.

Caution: debug_plugin intentionally breaks consensus rules to function; nodes using it may fork away from production chains.

Caution: stopping nodeos from inside the debugger will corrupt its database.

Debugging using vscode

You should have the following project tree:

<project root>/   <==== open this directory in vscode
   .vscode/
      c_cpp_properties.json
      launch.json
      settings.json
   CMakeLists.txt
   notify.cpp
   build/         (Created by build step)
      clsdk -> ....
      notify-debug.wasm
      notify.abi
      notify.wasm
      wasi-sdk -> ....

launch.json sets the following nodeos options. Adjust them to your needs:

-d data --config-dir config
--plugin eosio::chain_api_plugin
--plugin eosio::producer_api_plugin
--plugin eosio::debug_plugin
--subst clsdk/contracts/token.wasm:clsdk/contracts/token-debug.wasm
--subst notify.wasm:notify-debug.wasm
-e -p eosio

Open notify.cpp and set some break points. You may also add break points to build/clsdk/contracts/token/src/token.cpp.

Start the debugger. nodeos will start running. To see its log, switch to the "cppdbg: nodeos" terminal.

From another terminal, use these commands to install and exercise the contracts. The debugger should hit the breakpoints you set in the contracts and pause.

cd build

# Create some users
cleos create account eosio alice EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos create account eosio bob EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV

# Set up eosio.token
# Note: the build system created a symlink to clsdk for easy access to the token contract
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi eosio.token clsdk/contracts/token.abi
cleos set code eosio.token clsdk/contracts/token.wasm

cleos push action eosio.token create '["eosio", "1000000000.0000 EOS"]' -p eosio.token
cleos push action eosio.token issue '["eosio", "1000000000.0000 EOS", ""]' -p eosio
cleos push action eosio.token open '["alice", "4,EOS", "alice"]' -p alice
cleos push action eosio.token open '["bob", "4,EOS", "bob"]' -p bob
cleos push action eosio.token transfer '["eosio", "alice", "10000.0000 EOS", "have some"]' -p eosio
cleos push action eosio.token transfer '["eosio", "bob", "10000.0000 EOS", "have some"]' -p eosio

# Install the notify contract
cleos create account eosio notify EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi notify notify.abi
cleos set code notify notify.wasm

# Try out the notify contract
cleos push action eosio.token transfer '["alice", "notify", "300.0000 EOS", "for purchases"]' -p alice
cleos push action eosio.token transfer '["bob", "notify", "300.0000 EOS", "for purchases"]' -p bob

cleos push action notify buydog '["alice", "fido", "100.0000 EOS"]' -p alice
cleos push action notify buydog '["alice", "rex", "120.0000 EOS"]' -p alice
cleos push action notify buydog '["bob", "lambo", "70.0000 EOS"]' -p bob

# See the remaining balances and the purchased animals
cleos get table notify notify balance
cleos get table notify notify animal

Debugging functionality

The following are available:

  • breakpoints
  • step in
  • step out
  • step over
  • continue
  • call stack

The following are not available

  • examining variables
  • examining memory

Corrupted database recovery

The debugger can cause nodeos to corrupt its database. There are 2 options to recover from the corruption:

  • Wipe the database and start over: from the build directory, run rm -rf data
  • Force a replay. This can trigger breakpoints (helpful for reproductions). From the build directory, run rm -rf data/state data/blocks/reversible. Alternatively, add --hard-replay-blockchain to the nodeos options in launch.json.

You can start nodeos again in the debugger after doing one of the above.

Debugging using gdb command line

You should have the following project tree:

<project root>/
   CMakeLists.txt
   notify.cpp
   build/         (Created by build step)
      clsdk -> ....
      notify-debug.wasm
      notify.abi
      notify.wasm
      wasi-sdk -> ....

To start a debug session on the command line:

cd build
gdb -q --args                                                           \
   ./clsdk/bin/nodeos                                                   \
   -d data --config-dir config                                          \
   --plugin eosio::chain_api_plugin                                     \
   --plugin eosio::producer_api_plugin                                  \
   --plugin eosio::debug_plugin                                         \
   --subst clsdk/contracts/token.wasm:clsdk/contracts/token-debug.wasm  \
   --subst notify.wasm:notify-debug.wasm                                \
   -e -p eosio

Ignore No debugging symbols found in ...; it will load debugging symbols for the wasm files instead.

The following gdb commands set options gdb needs to function, set some breakpoints, and start nodeos.

handle SIG34 noprint
set breakpoint pending on
set substitute-path clsdk-wasi-sdk: wasi-sdk
set substitute-path clsdk: clsdk
b example_contract::notify_transfer
b example_contract::buydog
run

From another terminal, use these commands to install and exercise the contracts. The debugger should hit the breakpoints you set in the contracts and pause.

cd build

# Create some users
cleos create account eosio alice EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos create account eosio bob EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV

# Set up eosio.token
# Note: the build system created a symlink to clsdk for easy access to the token contract
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi eosio.token clsdk/contracts/token.abi
cleos set code eosio.token clsdk/contracts/token.wasm

cleos push action eosio.token create '["eosio", "1000000000.0000 EOS"]' -p eosio.token
cleos push action eosio.token issue '["eosio", "1000000000.0000 EOS", ""]' -p eosio
cleos push action eosio.token open '["alice", "4,EOS", "alice"]' -p alice
cleos push action eosio.token open '["bob", "4,EOS", "bob"]' -p bob
cleos push action eosio.token transfer '["eosio", "alice", "10000.0000 EOS", "have some"]' -p eosio
cleos push action eosio.token transfer '["eosio", "bob", "10000.0000 EOS", "have some"]' -p eosio

# Install the notify contract
cleos create account eosio notify EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi notify notify.abi
cleos set code notify notify.wasm

# Try out the notify contract; these should trigger breakpoints
cleos push action eosio.token transfer '["alice", "notify", "300.0000 EOS", "for purchases"]' -p alice
cleos push action eosio.token transfer '["bob", "notify", "300.0000 EOS", "for purchases"]' -p bob

cleos push action notify buydog '["alice", "fido", "100.0000 EOS"]' -p alice
cleos push action notify buydog '["alice", "rex", "120.0000 EOS"]' -p alice
cleos push action notify buydog '["bob", "lambo", "70.0000 EOS"]' -p bob

# See the remaining balances and the purchased animals
cleos get table notify notify balance
cleos get table notify notify animal

Debugging functionality

The following functionality is supported:

  • breakpoints (b)
  • step in (s)
  • step out (fin)
  • step over (n)
  • continue (c)
  • call stack (bt)

The following are not available:

  • examining variables
  • examining memory

Corrupted database recovery

The debugger can cause nodeos to corrupt its database. There are 2 options to recover from the corruption:

  • Wipe the database and start over: from the build directory, run rm -rf data
  • Force a replay. This can trigger breakpoints (helpful for reproductions). From the build directory, run rm -rf data/state data/blocks/reversible. Alternatively, add --hard-replay-blockchain to the nodeos options.

You can start nodeos again in the debugger after doing one of the above.

cltester: Getting Started

Contract modifications

To simplify testing, the contract's class definition and table definitions should be in a header file.

This example is based on the Debug Example, but has these additions:

  • The contract source is now split into testable.hpp and testable.cpp
  • CMakeLists.txt has a new rule to build tests.wasm from tests.cpp (below)
  • launch.json now launches the test cases in cltester instead of starting nodeos

The files:

Simple test case

tests.cpp:

// cltester definitions
#include <eosio/tester.hpp>

// contract definitions
#include "testable.hpp"

// Catch2 unit testing framework. https://github.com/catchorg/Catch2
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>

using namespace eosio;

TEST_CASE("No tokens")
{
   // This starts a blockchain. This is similar to running nodeos, but forces
   // creation of a new blockchain and offers more control.
   test_chain chain;

   // Install the testable contract. Some notes:
   // * create_code_account is like create_account (used below), except it adds
   //   eosio.code to the active authority.
   // * cltester doesn't need the ABI to operate, so we don't need to set it.
   chain.create_code_account("example"_n);
   chain.set_code("example"_n, "testable.wasm");

   // Create a user account
   chain.create_account("alice"_n);

   // Alice tries to buy a dog, but has no tokens
   // This verifies the appropriate error is produced
   expect(chain.as("alice"_n).trace<example::actions::buydog>(  //
              "alice"_n, "fido"_n, s2a("0.0000 EOS")),
          "Dogs cost more than that");
}

Running the test

This builds the contract and the test:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)

Use one of these to run the test:

cltester tests.wasm        # minimal logging
cltester -v tests.wasm     # show blockchain logging. This also
                           # shows any contract prints in green.

cltester: Debugging using vscode

You should have the following project tree:

<project root>/   <==== open this directory in vscode
   .vscode/
      c_cpp_properties.json
      launch.json
      settings.json
   CMakeLists.txt
   testable.hpp
   testable.cpp
   tests.cpp
   build/         (Created by build step)
      clsdk -> ....
      testable-debug.wasm
      testable.abi
      testable.wasm
      tests.wasm
      wasi-sdk -> ....

launch.json is configured to run the tests using cltester instead of starting nodeos. It sets the following cltester options:

--subst testable.wasm testable-debug.wasm
-v
tests.wasm

Open testable.cpp and set some break points. You may also add break points to tests.

Start the debugger. cltester will start running. To see its log, switch to the "cppdbg: cltester" terminal. vscode should stop at one of your breakpoints.

cltester: Debugging using gdb command line

You should have the following project tree:

<project root>/
   CMakeLists.txt
   testable.hpp
   testable.cpp
   tests.cpp
   build/         (Created by build step)
      clsdk -> ....
      testable-debug.wasm
      testable.abi
      testable.wasm
      tests.wasm
      wasi-sdk -> ....

To start a debug session on the command line:

cd build
gdb -q --args                                \
   ./clsdk/bin/cltester                      \
   --subst testable.wasm testable-debug.wasm \
   -v                                        \
   tests.wasm

Ignore No debugging symbols found in ...; it will load debugging symbols for the wasm files instead.

The following gdb commands set options gdb needs to function, set some breakpoints, and start cltester.

handle SIG34 noprint
set breakpoint pending on
set substitute-path clsdk-wasi-sdk: wasi-sdk
set substitute-path clsdk: clsdk
b example_contract::notify_transfer
b example_contract::buydog
run

cltester: Token Support

Our test cases need to interact with the token contract in order to fully test our example. clsdk comes with a cltester-ready version of the token contract.

Example files:

Test cases

This demonstrates the following:

  • Interacting with the token contract in tests
  • Running multiple tests using multiple chains
  • Creating helper functions to reduce repetition in tests

tests.cpp:

#include <eosio/tester.hpp>
#include <token/token.hpp>  // comes bundled with clsdk
#include "testable.hpp"

#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>

using namespace eosio;

// Set up the token contract
void setup_token(test_chain& t)
{
   t.create_code_account("eosio.token"_n);
   t.set_code("eosio.token"_n, CLSDK_CONTRACTS_DIR "token.wasm");

   // Create and issue tokens.
   t.as("eosio.token"_n).act<token::actions::create>("eosio"_n, s2a("1000000.0000 EOS"));
   t.as("eosio.token"_n).act<token::actions::create>("eosio"_n, s2a("1000000.0000 OTHER"));
   t.as("eosio"_n).act<token::actions::issue>("eosio"_n, s2a("1000000.0000 EOS"), "");
   t.as("eosio"_n).act<token::actions::issue>("eosio"_n, s2a("1000000.0000 OTHER"), "");
}

// Create and fund user accounts
void fund_users(test_chain& t)
{
   for (auto user : {"alice"_n, "bob"_n, "jane"_n, "joe"_n})
   {
      t.create_account(user);
      t.as("eosio"_n).act<token::actions::transfer>("eosio"_n, user, s2a("10000.0000 EOS"), "");
      t.as("eosio"_n).act<token::actions::transfer>("eosio"_n, user, s2a("10000.0000 OTHER"), "");
   }
}

// Set up the example contract
void setup_example(test_chain& t)
{
   t.create_code_account("example"_n);
   t.set_code("example"_n, "testable.wasm");
}

// Full setup for test chain
void setup(test_chain& t)
{
   setup_token(t);
   fund_users(t);
   setup_example(t);
}

TEST_CASE("Alice Attacks")
{
   // This is the first blockchain
   test_chain chain;
   setup(chain);

   // Alice tries to get a dog for free
   // This verifies the appropriate error is produced
   expect(chain.as("alice"_n).trace<example::actions::buydog>(  //
              "alice"_n, "fido"_n, s2a("0.0000 EOS")),
          "Dogs cost more than that");

   // Alice tries to buy a dog, but hasn't transferred any tokens to the contract
   expect(chain.as("alice"_n).trace<example::actions::buydog>(  //
              "alice"_n, "fido"_n, s2a("100.0000 EOS")),
          "user does not have a balance");

   // Alice tries to transfer an unsupported token to the contract
   expect(chain.as("alice"_n).trace<token::actions::transfer>(  //
              "alice"_n, "example"_n, s2a("100.0000 OTHER"), ""),
          "This contract does not deal with this token");

   // Alice transfers the correct token
   chain.as("alice"_n).act<token::actions::transfer>(  //
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "");

   // Alice tries to get sneaky with the wrong token
   expect(chain.as("alice"_n).trace<example::actions::buydog>(  //
              "alice"_n, "fido"_n, s2a("100.0000 OTHER")),
          "This contract does not deal with this token");
}

TEST_CASE("No duplicate dog names")
{
   // This is a different blockchain than used from the previous test
   test_chain chain;
   setup(chain);

   // Alice goes first
   chain.as("alice"_n).act<token::actions::transfer>(  //
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("alice"_n).act<example::actions::buydog>(  //
       "alice"_n, "fido"_n, s2a("100.0000 EOS"));
   chain.as("alice"_n).act<example::actions::buydog>(  //
       "alice"_n, "barf"_n, s2a("110.0000 EOS"));

   // Bob is next
   chain.as("bob"_n).act<token::actions::transfer>(  //
       "bob"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("bob"_n).act<example::actions::buydog>(  //
       "bob"_n, "wolf"_n, s2a("100.0000 EOS"));

   // Sorry, Bob
   expect(chain.as("bob"_n).trace<example::actions::buydog>(  //
              "bob"_n, "fido"_n, s2a("100.0000 EOS")),
          "could not insert object, most likely a uniqueness constraint was violated");
}

Running the test

This builds the contract, builds the tests, and runs the tests:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)
cltester -v tests.wasm

cltester: Reading Tables

Test cases can use the same database facilities (read-only) that contracts use. If contracts define all their tables in headers, then the test cases can get the table definitions from the headers.

Dumping table content

The tables in the example contract use a single scope. This makes it possible to iterate through all of the content in a table. The token contract uses a different scope for each user. This makes iteration infeasible. Instead, the test code has to explicitly specify each account to list the accounts' balances.

This extends the test cases in Token Support:

void dump_tokens(const std::vector<name> owners)
{
   printf("\nTokens\n=====\n");
   for (auto owner : owners)
   {
      token::accounts table("eosio.token"_n, owner.value);
      for (auto& account : table)
         printf("%-12s %s\n",
                owner.to_string().c_str(),
                account.balance.to_string().c_str());
   }
}

void dump_animals()
{
   printf("\nAnimals\n=====\n");
   example::animal_table table("example"_n, "example"_n.value);
   for (auto& animal : table)
      printf("%-12s %-12s %-12s %s\n",
             animal.name.to_string().c_str(),
             animal.type.to_string().c_str(),
             animal.owner.to_string().c_str(),
             animal.purchase_price.to_string().c_str());
}

TEST_CASE("Read Database")
{
   test_chain chain;
   setup(chain);

   chain.as("alice"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "fido"_n, s2a("100.0000 EOS"));
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "barf"_n, s2a("110.0000 EOS"));
   chain.as("bob"_n).act<token::actions::transfer>(
       "bob"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("bob"_n).act<example::actions::buydog>(
       "bob"_n, "wolf"_n, s2a("100.0000 EOS"));

   dump_tokens({"eosio"_n, "alice"_n, "bob"_n, "example"_n});
   dump_animals();
}

JSON form

clsdk comes with json conversion functions. These can aid dumping tables.

template <typename Table>
void dump_table(name contract, uint64_t scope)
{
   Table table(contract, scope);
   for (auto& record : table)
      std::cout << format_json(record) << "\n";
}

TEST_CASE("Read Database 2")
{
   test_chain chain;
   setup(chain);

   chain.as("alice"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "fido"_n, s2a("100.0000 EOS"));
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "barf"_n, s2a("110.0000 EOS"));
   chain.as("bob"_n).act<token::actions::transfer>(
       "bob"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("bob"_n).act<example::actions::buydog>(
       "bob"_n, "wolf"_n, s2a("100.0000 EOS"));

   printf("\nBalances\n=====\n");
   dump_table<example::balance_table>("example"_n, "example"_n.value);

   printf("\nAnimals\n=====\n");
   dump_table<example::animal_table>("example"_n, "example"_n.value);
}

Verifying table content

Test cases often need to verify table content is correct.

example::animal get_animal(name animal_name)
{
   example::animal_table table("example"_n, "example"_n.value);
   auto it = table.find(animal_name.value);
   if (it != table.end())
      return *it;
   else
      return {};  // return empty record if not found
}

TEST_CASE("Verify Animals")
{
   test_chain chain;
   setup(chain);

   chain.as("alice"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "fido"_n, s2a("100.0000 EOS"));
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "barf"_n, s2a("110.0000 EOS"));
   chain.as("bob"_n).act<token::actions::transfer>(
       "bob"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("bob"_n).act<example::actions::buydog>(
       "bob"_n, "wolf"_n, s2a("100.0000 EOS"));

   auto fido = get_animal("fido"_n);
   CHECK(fido.name == "fido"_n);
   CHECK(fido.type == "dog"_n);
   CHECK(fido.owner == "alice"_n);
   CHECK(fido.purchase_price == s2a("100.0000 EOS"));
}

The CHECK macro verifies its condition is met. The -s command-line option shows the progress of these checks:

$ cltester tests.wasm -s

...
/.../tests.cpp:78: PASSED:
  CHECK( fido.name == "fido"_n )
with expansion:
  fido == fido

/.../tests.cpp:79: PASSED:
  CHECK( fido.type == "dog"_n )
with expansion:
  dog == dog
...

Table caching issue

The above examples open tables within helper functions to avoid an issue with multi_index. multi_index caches data and so gets confused if a contract modifies a table while the tester has that table currently open.

cltester: as/act/trace

The test_chain class supports this syntax for pushing single-action transactions:

chain.as("alice"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "memo");

as

as(account) returns an object that represents an account's active authority. as also supports other authorities:

chain.as("alice"_n, "owner"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "memo");

act

act<action wrapper>(action args) creates, signs, and executes a single-action transaction. It also verifies the transaction succeeded. If it fails, it aborts the test with an error message.

The contract headers use EOSIO_ACTIONS(...) to define the action wrappers, e.g. token::actions::transfer or example::actions::buydog. The wrappers record the default contract name (e.g. eosio.token), the name of the action (e.g. transfer), and the argument types. This allows strong type safety. It also bypasses any need for ABIs.

act signs with default_priv_key - a well-known key used for testing (5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3). This key pairs with default_pub_key (EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV). Both the create_account and create_code_account methods create accounts with default_pub_key.

trace

Like act, trace<action wrapper>(action args) creates, signs, and executes a single-action transaction. Unlike act, trace does not verify success. Instead, it returns the transaction's trace.

We could display the trace:

auto result = chain.as("alice"_n).trace<example::actions::buydog>(
    "alice"_n, "fido"_n, s2a("100.0000 OTHER"));
std::cout << format_json(result) << "\n";

This produces output like the following:

{
    "id": "F4EE6CACEF935889E35355568C492409C6F4535565B0B801EC31352DEFAA40F3",
    "status": "hard_fail",
    "cpu_usage_us": 0,
    "net_usage_words": 0,
    "elapsed": "62",
    "net_usage": "124",
    "scheduled": false,
    "action_traces": [
        {
            "action_ordinal": 1,
            "creator_action_ordinal": 0,
            "receipt": null,
            "receiver": "example",
            "act": {
                "account": "example",
                "name": "buydog",
                "authorization": [
                    {
                        "actor": "alice",
                        "permission": "active"
                    }
                ],
                "data": [...]
            },
            "context_free": false,
            "elapsed": "34",
            "console": "",
            "account_ram_deltas": [],
            "account_disk_deltas": [],
            "except": "eosio_assert_message assertion failure (3050003)\nassertion failure with message: This contract does not deal with this token\npending console output: \n",
            "error_code": "10000000000000000000",
            "return_value": []
        }
    ],
    "account_ram_delta": null,
    "except": "eosio_assert_message assertion failure (3050003)\nassertion failure with message: This contract does not deal with this token\npending console output: \n",
    "error_code": "10000000000000000000",
    "failed_dtrx_trace": []
}

expect

expect verifies that a transaction trace's except field contains within it an expected error message. If the the transaction succeeded, or the transaction failed but with a different message, then expect aborts the test with an error message. expect does a substring match.

expect(chain.as("alice"_n).trace<example::actions::buydog>(
            "alice"_n, "fido"_n, s2a("100.0000 OTHER")),
         "This contract does not deal with this token");

with_code

The action wrappers provide a default account name that the contract is normally installed on. e.g. the token wrappers assume eosio.token. with_code overrides this default.

This example sets up a fake EOS token to try to fool our example code.

// The hacker.token account runs the token contract
chain.create_code_account("hacker.token"_n);
chain.set_code("hacker.token"_n, CLSDK_CONTRACTS_DIR "token.wasm");
chain.as("hacker.token"_n)
      .with_code("hacker.token"_n)
      .act<token::actions::create>("hacker.token"_n, s2a("1000000.0000 EOS"));
chain.as("hacker.token"_n)
      .with_code("hacker.token"_n)
      .act<token::actions::issue>("hacker.token"_n, s2a("1000000.0000 EOS"), "");

// Give fake EOS to Alice
chain.as("hacker.token"_n)
      .with_code("hacker.token"_n)
      .act<token::actions::transfer>("hacker.token"_n, "alice"_n, s2a("10000.0000 EOS"), "");

// Alice transfers fake EOS to the example contract
chain.as("alice"_n)
      .with_code("hacker.token"_n)
      .act<token::actions::transfer>(
         "alice"_n, "example"_n, s2a("300.0000 EOS"), "");

// The contract didn't credit her account with the fake EOS
expect(chain.as("alice"_n).trace<example::actions::buydog>(
            "alice"_n, "fido"_n, s2a("100.0000 EOS")),
         "user does not have a balance");

as() variables

as() returns an object which can be stored in a variable to reduce repetition.

auto alice = chain.as("alice"_n);
alice.act<token::actions::transfer>(
    "alice"_n, "example"_n, s2a("300.0000 EOS"), "");
alice.act<example::actions::buydog>(
    "alice"_n, "fido"_n, s2a("100.0000 EOS"));
alice.act<example::actions::buydog>(
    "alice"_n, "barf"_n, s2a("110.0000 EOS"));

cltester: BIOS and Chain Configuration

cltester comes with multiple bios contracts which support different sets of protocol features.

NameRequired FeaturesAdditional Actions
biosPREACTIVATE_FEATURE
bios2PREACTIVATE_FEATURE, WTMSIG_BLOCK_SIGNATURESsetprods
bios3PREACTIVATE_FEATURE, WTMSIG_BLOCK_SIGNATURES, BLOCKCHAIN_PARAMETERS, ACTION_RETURN_VALUE, CONFIGURABLE_WASM_LIMITS2setprods, wasmcfg, enhanced setparams

cltester always activates PREACTIVATE_FEATURE; bios can be installed as soon as the chain is created. The other bioses need additional protocol features activated before they can be installed. The activate action in bios can activate these features. A helper method helps activate these in bulk.

Activating Protocol Features

bios contract headers include a helper function (activate) which activates multiple protocol features.

#include <bios/bios.hpp>

TEST_CASE("activate")
{
   test_chain chain;

   // Load bios
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios.wasm");

   bios::activate(chain, {
      // Features available in 2.0
      eosio::feature::only_link_to_existing_permission,
      eosio::feature::forward_setcode,
      eosio::feature::wtmsig_block_signatures,
      eosio::feature::replace_deferred,
      eosio::feature::no_duplicate_deferred_id,
      eosio::feature::ram_restrictions,
      eosio::feature::webauthn_key,
      eosio::feature::disallow_empty_producer_schedule,
      eosio::feature::only_bill_first_authorizer,
      eosio::feature::restrict_action_to_self,
      eosio::feature::fix_linkauth_restriction,
      eosio::feature::get_sender,

      // Features added in 3.0
      eosio::feature::blockchain_parameters,
      eosio::feature::action_return_value,
      eosio::feature::get_code_hash,
      eosio::feature::configurable_wasm_limits2,
   });

   // Load bios3
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios3.wasm");

   // Use the chain...
}

Setting Consensus Parameters (bios, bios2)

The setparams action sets consensus parameters which are available in nodeos 2.0 and earlier.

#include <bios/bios.hpp>

TEST_CASE("setparams")
{
   test_chain chain;

   // Load bios
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios.wasm");

   // Allow deeper inline actions. Sets all other parameters to the default.
   chain.as("eosio"_n).act<bios::actions::setparams>(
      blockchain_parameters{
         .max_inline_action_depth = 10});

   // Use the chain...
}

eosio::blockchain_parameters has the following definition:

struct blockchain_parameters
{
   constexpr static int percent_1 = 100;  // 1 percent

   uint64_t max_block_net_usage = 1024 * 1024;
   uint32_t target_block_net_usage_pct = 10 * percent_1;
   uint32_t max_transaction_net_usage = max_block_net_usage / 2;
   uint32_t base_per_transaction_net_usage = 12;
   uint32_t net_usage_leeway = 500;
   uint32_t context_free_discount_net_usage_num = 20;
   uint32_t context_free_discount_net_usage_den = 100;
   uint32_t max_block_cpu_usage = 200'000;
   uint32_t target_block_cpu_usage_pct = 10 * percent_1;
   uint32_t max_transaction_cpu_usage = 3 * max_block_cpu_usage / 4;
   uint32_t min_transaction_cpu_usage = 100;
   uint32_t max_transaction_lifetime = 60 * 60;
   uint32_t deferred_trx_expiration_window = 10 * 60;
   uint32_t max_transaction_delay = 45 * 24 * 3600;
   uint32_t max_inline_action_size = 512 * 1024;
   uint16_t max_inline_action_depth = 4;
   uint16_t max_authority_depth = 6;
};

Setting Consensus Parameters (bios3)

The setparams action in bios3 adds an additional field: max_action_return_value_size.

#include <bios/bios.hpp>
#include <bios3/bios3.hpp>

TEST_CASE("setparams_bios3")
{
   test_chain chain;
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios.wasm");
   bios::activate(chain, {
      eosio::feature::wtmsig_block_signatures,
      eosio::feature::blockchain_parameters,
      eosio::feature::action_return_value,
      eosio::feature::configurable_wasm_limits2,
   });
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios3.wasm");

   // Allow larger return sizes. Sets all other parameters to the default.
   chain.as("eosio"_n).act<bios3::actions::setparams>(
      bios3::blockchain_parameters_v1{
         .max_action_return_value_size = 1024});

   // Use the chain...
}

Expanding WASM limits (bios3)

The wasmcfg action in bios3 expands the WASM limits.

#include <bios/bios.hpp>
#include <bios3/bios3.hpp>

TEST_CASE("wasmcfg")
{
   test_chain chain;
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios.wasm");
   bios::activate(chain, {
      eosio::feature::wtmsig_block_signatures,
      eosio::feature::blockchain_parameters,
      eosio::feature::action_return_value,
      eosio::feature::configurable_wasm_limits2,
   });
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios3.wasm");

   // low, default (same as low), or high
   chain.as("eosio"_n).act<bios3::actions::wasmcfg>("high"_n);

   // Use the chain...
}

cltester: Block Control

The test_chain class gives you control over block production, including control over time.

start_block

At any point, you can start producing a new block:

chain.start_block();

This finishes producing the current block (if one is being produced), then starts producing a new one. All transactions after this point go into the new block.

You can skip time:

chain.start_block(2000); // skip 2000ms-worth of blocks
  • 0 skips nothing; the new block is 500ms after the current block being produced (if any), or 500ms after the previous block.
  • 500 skips 1 block
  • 1000 skips 2 blocks

You can also skip to a specific time:

chain.start_block("2020-07-03T15:29:59.500");

or

eosio::time_point t = ...;
chain.start_block(t);

Note when skipping time: start_block creates an empty block immediately before the new one. This allows TAPoS to operate correctly after skipping large periods of time.

finish_block

At any point, you can stop producing the current block:

chain.finish_block();

After you call finish_block, the system is in a state where no blocks are being produced. The following cause the system to start producing a new block:

  • using start_block
  • pushing a transaction
  • using finish_block again. This causes the system to start a new block then finish it.

Getting the head block time

This gets the head block time as a time_point:

auto t = chain.get_head_block_info().timestamp.to_time_point();

Note: the head block is not the block that's currently being produced. Instead, it's the last block which was finished.

You can display the time:

std::cout << convert_to_json(t) << "\n";

You can also do arithmetic on time:

chain.start_block(
   chain.get_head_block_info().timestamp.to_time_point() +
   eosio::days(8) + eosio::hours(1));

Fixing duplicate transactions

It's easy to create a test which tries to push duplicate transactions. Call start_block before the duplicate transaction to solve this.

Fixing full blocks

It's easy to overfill a block, causing a test failure. Use start_block to solve this.

cltester: Starting Nodeos

cltester uses chainlib from nodeos, but cltester isn't nodeos, so is missing some functionality. e.g. it can't connect to nodes using p2p, and it can't serve json rpc requests. It can, however, spawn nodeos on a chain which cltester created. This can help with system-wide testing, e.g. testing nodejs and web apps.

TEST_CASE("start nodeos")
{
   // Prepare a chain
   test_chain chain;
   setup(chain);

   // cltester doesn't need ABIs, but most other tools do
   chain.set_abi("eosio.token"_n, CLSDK_CONTRACTS_DIR "token.abi");
   chain.set_abi("example"_n, "testable.abi");

   // Alice buys some dogs
   chain.as("alice"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "fido"_n, s2a("100.0000 EOS"));
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "barf"_n, s2a("110.0000 EOS"));

   // Make the above irreversible. This causes the transactions to
   // go into the block log.
   chain.finish_block();
   chain.finish_block();

   // Copy blocks.log into a fresh directory for nodeos to use
   eosio::execute("rm -rf example_chain");
   eosio::execute("mkdir -p example_chain/blocks");
   eosio::execute("cp " + chain.get_path() + "/blocks/blocks.log example_chain/blocks");

   // Run nodeos
   eosio::execute(
       "nodeos -d example_chain "
       "--config-dir example_config "
       "--plugin eosio::chain_api_plugin "
       "--access-control-allow-origin \"*\" "
       "--access-control-allow-header \"*\" "
       "--http-validate-host 0 "
       "--http-server-address 0.0.0.0:8888 "
       "--contracts-console "
       "-e -p eosio");
}

After running the above, cleos should now function:

cleos push action example buydog '["alice", "spot", "90.0000 EOS"]' -p alice
cleos get table example example balance
cleos get table example example animal

cltester: System Contract

This example shows how to load and activate the eosio.system contract on a test chain. This is a simplified setup which isn't suitable for public chains. All accounts use the default public key. This doesn't activate optional features (REX, powerup, etc.). It spawns nodeos on the activated chain with 21 producers.

#include <eosio/tester.hpp>

#include <bios/bios.hpp>
#include <token/token.hpp>

#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>

using namespace eosio;

// Where eosio.system.wasm and eosio.system.abi live
const std::string system_path = "/path/to/built/eosio.system";

const std::vector system_accounts = {
    "eosio.bpay"_n, "eosio.names"_n,  "eosio.ram"_n,   "eosio.ramfee"_n,
    "eosio.rex"_n,  "eosio.saving"_n, "eosio.stake"_n, "eosio.vpay"_n,
};

const std::vector producers = {
    "bpaaaaaaaaaa"_n, "bpbbbbbbbbbb"_n, "bpcccccccccc"_n, "bpdddddddddd"_n, "bpeeeeeeeeee"_n,
    "bpffffffffff"_n, "bpgggggggggg"_n, "bphhhhhhhhhh"_n, "bpiiiiiiiiii"_n, "bpjjjjjjjjjj"_n,
    "bpkkkkkkkkkk"_n, "bpllllllllll"_n, "bpmmmmmmmmmm"_n, "bpnnnnnnnnnn"_n, "bpoooooooooo"_n,
    "bppppppppppp"_n, "bpqqqqqqqqqq"_n, "bprrrrrrrrrr"_n, "bpssssssssss"_n, "bptttttttttt"_n,
    "bpuuuuuuuuuu"_n,
};

TEST_CASE("Activate eosio.system")
{
   test_chain chain;

   // Create accounts
   for (auto account : system_accounts)
      chain.create_account(account);
   for (auto account : producers)
      chain.create_account(account);
   chain.create_account("whale"_n);

   // Load bios and activate features
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios.wasm");
   bios::activate(chain, {
      // Features available in 2.0
      feature::only_link_to_existing_permission,
      feature::forward_setcode,
      feature::wtmsig_block_signatures,
      feature::replace_deferred,
      feature::no_duplicate_deferred_id,
      feature::ram_restrictions,
      feature::webauthn_key,
      feature::disallow_empty_producer_schedule,
      feature::only_bill_first_authorizer,
      feature::restrict_action_to_self,
      feature::fix_linkauth_restriction,
      feature::get_sender,

      // Features added in 3.0
      feature::blockchain_parameters,
      feature::action_return_value,
      feature::get_code_hash,
      feature::configurable_wasm_limits2,
   });

   // Create token
   chain.create_code_account("eosio.token"_n);
   chain.set_code("eosio.token"_n, CLSDK_CONTRACTS_DIR "token.wasm");
   chain.set_abi("eosio.token"_n, CLSDK_CONTRACTS_DIR "token.abi");
   chain.as("eosio.token"_n).act<token::actions::create>("eosio"_n, s2a("1000000000.0000 EOS"));
   chain.as("eosio"_n).act<token::actions::issue>("eosio"_n, s2a("1000000000.0000 EOS"), "");

   // Load and initialize system contract
   chain.set_code("eosio"_n, system_path + "/eosio.system.wasm");
   chain.set_abi("eosio"_n, system_path + "/eosio.system.abi");
   chain.transact({action{{{"eosio"_n, "active"_n}},
                          "eosio"_n,
                          "init"_n,
                          std::tuple{varuint32(0), symbol("EOS", 4)}}});

   // Register producers
   for (auto prod : producers)
   {
      chain.transact({action{{{prod, "active"_n}},
                             "eosio"_n,
                             "regproducer"_n,
                             std::tuple{prod, chain.default_pub_key, std::string{}, 0}}});
   }

   // Whale activates system contract by voting
   chain.as("eosio"_n).act<token::actions::transfer>("eosio"_n, "whale"_n,
                                                     s2a("500000000.0000 EOS"), "");
   chain.transact({action{{{"whale"_n, "active"_n}},
                          "eosio"_n,
                          "buyrambytes"_n,
                          std::tuple{"whale"_n, "whale"_n, 10000}}});
   chain.transact({action{{{"whale"_n, "active"_n}},
                          "eosio"_n,
                          "delegatebw"_n,
                          std::tuple{"whale"_n, "whale"_n, s2a("75000000.0000 EOS"),
                                     s2a("75000000.0000 EOS"), false}}});
   chain.transact({action{{{"whale"_n, "active"_n}},
                          "eosio"_n,
                          "voteproducer"_n,
                          std::tuple{"whale"_n, ""_n, producers}}});

   // Run nodeos
   chain.finish_block();
   chain.finish_block();
   eosio::execute("rm -rf example_chain");
   eosio::execute("mkdir -p example_chain/blocks");
   eosio::execute("cp " + chain.get_path() + "/blocks/blocks.log example_chain/blocks");
   eosio::execute(
       "nodeos -d example_chain "
       "--config-dir example_config "
       "--plugin eosio::chain_api_plugin "
       "--access-control-allow-origin \"*\" "
       "--access-control-allow-header \"*\" "
       "--http-validate-host 0 "
       "--http-server-address 0.0.0.0:8888 "
       "--contracts-console "
       "-e -p eosio "
       "-p bpaaaaaaaaaa -p bpbbbbbbbbbb -p bpcccccccccc -p bpdddddddddd -p bpeeeeeeeeee "
       "-p bpffffffffff -p bpgggggggggg -p bphhhhhhhhhh -p bpiiiiiiiiii -p bpjjjjjjjjjj "
       "-p bpkkkkkkkkkk -p bpllllllllll -p bpmmmmmmmmmm -p bpnnnnnnnnnn -p bpoooooooooo "
       "-p bppppppppppp -p bpqqqqqqqqqq -p bprrrrrrrrrr -p bpssssssssss -p bptttttttttt "
       "-p bpuuuuuuuuuu");
}

cltester: Hostile Takeover

cltester can fork the state of an existing chain. This example loads an EOS snapshot, replaces the eosio and producer keys, and launches a nodeos instance which acts as 21 producers.

TEST_CASE("Takeover")
{
   std::cout << "Loading snapshot...\n";

   // This constructor loads a snapshot. The second argument is the max database size.
   test_chain chain{"/home/todd/work/snapshot-2021-11-21-22-eos-v4-0216739301.bin",
                    uint64_t(20) * 1024 * 1024 * 1024};

   // Replace production keys and eosio keys. These functions don't push any
   // transactions. Instead, they directly modify the chain state in a way which
   // violates consensus rules.
   std::cout << "Replacing keys...\n";
   chain.replace_producer_keys(test_chain::default_pub_key);
   chain.replace_account_keys("eosio"_n, "owner"_n, test_chain::default_pub_key);
   chain.replace_account_keys("eosio"_n, "active"_n, test_chain::default_pub_key);

   // We replaced the production keys, but the system contract can switch
   // them back. Let's fix that.
   for (auto prod :
        {"atticlabeosb"_n, "aus1genereos"_n, "big.one"_n,      "binancestake"_n, "bitfinexeos1"_n,
         "blockpooleos"_n, "eosasia11111"_n, "eoscannonchn"_n, "eoseouldotio"_n, "eosflytomars"_n,
         "eoshuobipool"_n, "eosinfstones"_n, "eosiosg11111"_n, "eoslaomaocom"_n, "eosnationftw"_n,
         "hashfineosio"_n, "newdex.bp"_n,    "okcapitalbp1"_n, "starteosiobp"_n, "whaleex.com"_n,
         "zbeosbp11111"_n})
   {
      std::cout << "    " << prod.to_string() << "\n";
      chain.replace_account_keys(prod, "owner"_n, test_chain::default_pub_key);
      chain.replace_account_keys(prod, "active"_n, test_chain::default_pub_key);
      chain.transact({
          action{{{"eosio"_n, "owner"_n}}, "eosio.null"_n, "free.trx"_n, std::tuple{}},
          action{{{prod, "owner"_n}},
                 "eosio"_n,
                 "regproducer"_n,
                 std::make_tuple(prod, test_chain::default_pub_key, std::string("url"),
                                 uint16_t(1234))},
      });
   }

   // Make a donation. This works because eosio.rex delegates to eosio,
   // and we replaced eosio's keys.
   chain.transact({
       action{{{"eosio"_n, "owner"_n}}, "eosio.null"_n, "free.trx"_n, std::tuple{}},
       action{{{"eosio.rex"_n, "owner"_n}},
              "eosio.token"_n,
              "transfer"_n,
              std::make_tuple("eosio.rex"_n, "genesis.eden"_n, s2a("50000000.0000 EOS"),
                              std::string("donate"))},
   });

   // Produce the block
   chain.finish_block();

   // shut down the chain so we can safely copy the database
   std::cout << "Shutdown...\n";
   chain.shutdown();

   // Copy everything into a fresh directory for nodeos to use
   std::cout << "Copy...\n";
   eosio::execute("rm -rf forked_chain");
   eosio::execute("cp -r " + chain.get_path() + " forked_chain");

   // Run nodeos. We must use the build which is packaged with clsdk since we're
   // loading the non-portable database.
   std::cout << "Start nodeos...\n";
   eosio::execute(
       "./clsdk/bin/nodeos "
       "-d forked_chain "
       "--config-dir example_config "
       "--plugin eosio::chain_api_plugin "
       "--access-control-allow-origin \"*\" "
       "--access-control-allow-header \"*\" "
       "--http-validate-host 0 "
       "--http-server-address 0.0.0.0:8888 "
       "--contracts-console "
       "-e "
       "-p atticlabeosb "
       "-p aus1genereos "
       "-p big.one "
       "-p binancestake "
       "-p bitfinexeos1 "
       "-p blockpooleos "
       "-p eosasia11111 "
       "-p eoscannonchn "
       "-p eoseouldotio "
       "-p eosflytomars "
       "-p eoshuobipool "
       "-p eosinfstones "
       "-p eosiosg11111 "
       "-p eoslaomaocom "
       "-p eosnationftw "
       "-p hashfineosio "
       "-p newdex.bp "
       "-p okcapitalbp1 "
       "-p starteosiobp "
       "-p whaleex.com "
       "-p zbeosbp11111 ");
}