Skip to main content

Mandos traces

Overview

A Mandos trace is an auto-generated scenario file (.scen.json) derived from the logic within an integration test. We can generate a Mandos trace in the contract's root directory by initiating the trace at the start of the test and saving it as a JSON file at the end, as demonstrated below:

    world.start_trace();

// integration test logic

world.write_scenario_trace("trace1.scen.json");

Auto-generating Mandos scenarios is particularly useful for testing the contract against both Go and Rust VMs. Writing the integration test in Rust allows us to test the contract against the Rust VM. By generating a scenario trace, we can then test the scenario against the Go VM without manually rewriting the logic.

Example

Let's explore a use-case where generating traces proves beneficial. Below is the adder contract created using the sc-meta new template --adder command:

#[multiversx_sc::contract]
pub trait Adder {
#[view(getSum)]
#[storage_mapper("sum")]
fn sum(&self) -> SingleValueMapper<BigUint>;

#[init]
fn init(&self, initial_value: BigUint) {
self.sum().set(initial_value);
}

#[upgrade]
fn upgrade(&self, initial_value: BigUint) {
self.init(initial_value);
}

/// Add desired amount to the storage variable.
#[endpoint]
fn add(&self, value: BigUint) {
self.sum().update(|sum| *sum += value);
}
}

To ensure comprehensive testing of the contract, we need to test interactions across multiple environments.

note

Using contract templates simplifies the process of creating and testing smart contracts. For example, executing the sc-meta new --template adder command generates an adder contract with various types of tests already included.

The first step is to create Rust integration tests:

#[test]
fn adder_blackbox() {
let mut world = world();

world.start_trace();

world.account(OWNER_ADDRESS).nonce(1);

let new_address = world
.tx()
.from(OWNER_ADDRESS)
.typed(adder_proxy::AdderProxy)
.init(5u32)
.code(CODE_PATH)
.new_address(ADDER_ADDRESS)
.returns(ReturnsNewAddress)
.run();

assert_eq!(new_address, ADDER_ADDRESS.to_address());

world
.query()
.to(ADDER_ADDRESS)
.typed(adder_proxy::AdderProxy)
.sum()
.returns(ExpectValue(5u32))
.run();

world
.tx()
.from(OWNER_ADDRESS)
.to(ADDER_ADDRESS)
.typed(adder_proxy::AdderProxy)
.add(1u32)
.run();

world
.query()
.to(ADDER_ADDRESS)
.typed(adder_proxy::AdderProxy)
.sum()
.returns(ExpectValue(6u32))
.run();

world.check_account(OWNER_ADDRESS);

world
.check_account(ADDER_ADDRESS)
.check_storage("str:sum", "6");

world
.tx()
.from(OWNER_ADDRESS)
.to(ADDER_ADDRESS)
.typed(adder_proxy::AdderProxy)
.upgrade(100u64)
.code(CODE_PATH)
.run();

world
.check_account(ADDER_ADDRESS)
.check_storage("str:sum", "100");

world.write_scenario_trace("trace1.scen.json");
}

This test provides a small scenario that covers all endpoints and verifies the results. The .start_trace() and .write_scenario_trace(FILE_PATH) functions ensure that all actions between them are captured and converted into Mandos steps.

After running the test, the newly generated trace1.scen.json will contain the following:

{
"steps": [
{
"step": "setState",
"accounts": {
"address:owner": {
"nonce": "1"
}
}
},
{
"step": "setState",
"newAddresses": [
{
"creatorAddress": "address:owner",
"creatorNonce": "1",
"newAddress": "sc:adder"
}
]
},
{
"step": "scDeploy",
"id": "",
"tx": {
"from": "address:owner",
"contractCode": "mxsc:output/adder.mxsc.json",
"arguments": [
"0x05"
],
"gasLimit": "5,000,000"
},
"expect": {
"out": [],
"status": "0"
}
},
{
"step": "scQuery",
"id": "",
"tx": {
"to": "sc:adder",
"function": "getSum",
"arguments": []
},
"expect": {
"out": [
"0x05"
],
"status": "0"
}
},
{
"step": "scCall",
"id": "",
"tx": {
"from": "address:owner",
"to": "sc:adder",
"function": "add",
"arguments": [
"0x01"
],
"gasLimit": "5,000,000"
},
"expect": {
"out": [],
"status": "0"
}
},
{
"step": "scQuery",
"id": "",
"tx": {
"to": "sc:adder",
"function": "getSum",
"arguments": []
},
"expect": {
"out": [
"0x06"
],
"status": "0"
}
},
{
"step": "checkState",
"accounts": {
"address:owner": {
"storage": "*",
"code": "*",
"owner": "*"
}
}
},
{
"step": "checkState",
"accounts": {
"sc:adder": {
"storage": {
"str:sum": "6"
},
"code": "*",
"owner": "*"
}
}
},
{
"step": "scCall",
"id": "",
"tx": {
"from": "address:owner",
"to": "sc:adder",
"function": "upgrade",
"arguments": [
"0x64"
],
"gasLimit": "5,000,000"
},
"expect": {
"out": [],
"status": "0"
}
},
{
"step": "checkState",
"accounts": {
"sc:adder": {
"storage": {
"str:sum": "100"
},
"code": "*",
"owner": "*"
}
}
}
]
}

As seen above, all blackbox actions are now captured as Mandos steps, simply by including the trace in the test.

However, testing only against the Rust VM alone is insufficient since the actual VM running on the nodes is the Go VM. The Mandos file generated from the trace enables easy testing against a Go VM backend (not the actual Go VM) using the VmGoBackend:

use multiversx_sc_scenario::*;

fn world() -> ScenarioWorld {
ScenarioWorld::vm_go() // VmGoBackend
}

#[test]
fn trace_one_go() {
world().run("trace1.scen.json"); // run trace
}

Mandos tests can also run against the Rust VM, bypassing the Rust testing framework syntax:

use multiversx_sc_scenario::*;

fn world() -> ScenarioWorld {
let mut blockchain = ScenarioWorld::new(); // ScenarioWorld struct

blockchain.register_contract("mxsc:output/adder.mxsc.json", adder::ContractBuilder);
blockchain
}

#[test]
fn trace_one_rs() {
world().run("trace1.scen.json"); // run trace
}

The key advantage of Mandos tests is their seamless integration with the actual Go VM. Mandos serves as the unified "language" across our systems, allowing us to run the same scenarios in different environments. To ensure comprehensive VM-related testing, we can simply add our generated trace1.scen.json file to the collection of Mandos tests already present and execute the entire scenario against the Go VM.