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'snodeos
executable - Create deterministic tests using
cltester
, which comes with Contract Lab - Debug contracts and tests using
cltester
- Bootstrap test chains using
cltester
and spawn anodeos
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 executedebug.wasm
whenever it would otherwise executecontract.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, runrm -rf data
- Force a replay. This can trigger breakpoints (helpful for reproductions). From the
build
directory, runrm -rf data/state data/blocks/reversible
. Alternatively, add--hard-replay-blockchain
to the nodeos options inlaunch.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, runrm -rf data
- Force a replay. This can trigger breakpoints (helpful for reproductions). From the
build
directory, runrm -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
andtestable.cpp
- CMakeLists.txt has a new rule to build
tests.wasm
fromtests.cpp
(below) launch.json
now launches the test cases in cltester instead of starting nodeos
The files:
- testable.hpp
- testable.cpp
- CMakeLists.txt
- .vscode/c_cpp_properties.json
- .vscode/settings.json
- .vscode/launch.json
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:
- testable.hpp
- testable.cpp
- tests.cpp
- CMakeLists.txt
- .vscode/c_cpp_properties.json
- .vscode/settings.json
- .vscode/launch.json
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.
Name | Required Features | Additional Actions |
---|---|---|
bios | PREACTIVATE_FEATURE | |
bios2 | PREACTIVATE_FEATURE , WTMSIG_BLOCK_SIGNATURES | setprods |
bios3 | PREACTIVATE_FEATURE , WTMSIG_BLOCK_SIGNATURES , BLOCKCHAIN_PARAMETERS , ACTION_RETURN_VALUE , CONFIGURABLE_WASM_LIMITS2 | setprods , 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 ");
}