DEV Community

Christian Rogobete
Christian Rogobete

Posted on

Soroban contract with TinyGo

Invoking a soroban contract built with TinyGo

This post describes my experience with building a simple smart contract for Soroban with TinyGo, deploying and invoking its function with the soroban-cli and on futurenet.

I started on 5 February 2023 and at that time, the available TinyGo version was 0.26.0. The Soroban interface version of the host functions was 27.

First, I installed TinyGo and developed a very simple smart contract. It adds two uint32 values and return the result.

Contract code

package main

//export add
func add(x uint64, y uint64) uint64 {
    res := toU32(x) + toU32(y)
    return fromU32(res)

// main is required for the `wasm` target, even if it isn't used.
func  main() {}

// Extracts the 32-bit unsigned integer from a host value that represents a 32-bit unsigned integer.
func toU32(val uint64) uint32 {
    return  uint32(val >> 4)

// Creates a host value that represents a 32-bit unsigned integer.
func fromU32(val uint32) uint64 {
    return  addTagToBody(0, uint64(val))

// Adds a given tag to a host value body.
func  addTagToBody(tag uint8, body uint64) uint64 {
    return (body << 4) | uint64((tag << 1)) | 1
Enter fullscreen mode Exit fullscreen mode

As you can see, our function is called add. It should add two uint32 values. As input parameters, it receives 2 host values (uint64) and returns the result as host value too.

First, we convert the received host values to uint32, add them, and then return the result as host value.

Building with tinyGo

Building with TinyGo first generates a huge wasm file:

tinygo build -o add.wasm -target=wasm -wasm-abi=generic add.go

-rwxr-xr-x 1 chris staff 56506 7 Feb 14:34 add.wasm

However, TinyGo offers a few build flags and after some experimentation, I ended up with this command:

tinygo build -o add.wasm -target=wasm -wasm-abi=generic -scheduler=none --no-debug -panic=trap -opt z -gc=leaking add.go

First issue here: -gc! It lets one specify the memory manager (garbage collector) to be used. There are 3 options available: none, leaking and conservative. I wanted to use none of course, but unfortunately that did not work out. It gave me the following error:

tinygo:wasm-ld: error: /var/folders/w4/lcgfp2855g3f2k2_zs16tbnr0000gn/T/tinygo1603362656/main.o: undefined symbol: runtime.alloc
failed to run tool: wasm-ld
error: failed to link /var/folders/w4/lcgfp2855g3f2k2_zs16tbnr0000gn/T/tinygo1603362656/main: exit status 1
Enter fullscreen mode Exit fullscreen mode

More about this later ...

So I ended up with the command above, generating a smaller (but still huge) wasm file:

-rwxr-xr-x   1 chris  staff  3355  7 Feb 15:03 add.wasm
Enter fullscreen mode Exit fullscreen mode

If one converts the wasm file to wat (textual representation), then one can read its content. It contains 1200 lines of code. See: gist: add.wat.

A lot of strange logic and 4 not so welcome exports:

(func $malloc.command_export (export "malloc") (type $t1) (param $p0 i32) (result i32)
    (call $malloc
      (local.get $p0))
    (call $__wasm_call_dtors))
  (func $free.command_export (export "free") (type $t5) (param $p0 i32)
    (call $free
      (local.get $p0))
    (call $__wasm_call_dtors))
  (func $calloc.command_export (export "calloc") (type $t6) (param $p0 i32) (param $p1 i32) (result i32)
    (call $calloc
      (local.get $p0)
      (local.get $p1))
    (call $__wasm_call_dtors))
  (func $realloc.command_export (export "realloc") (type $t6) (param $p0 i32) (param $p1 i32) (result i32)
Enter fullscreen mode Exit fullscreen mode

However, I decided to go forward with it and see if I can deploy the contract and invoke its add function using the soroban-cli.

Adding meta and contract spec

As described in the Soroban docs, the wasm module needs to contain 2 custom sections, meta, describing the host interface version and contract spec, describing the exported functions.

In AssemblyScript, the compiler frontend (asc) provides a mechanism to hook into the compilation process before, while and after the module is being compiled. This can be used with so called Transforms and Hooks, which allowed me to add the 2 custom sections to the module in the AssemblyScript SDK.

However, in TinyGo there is no such mechanism. After a question in the TinyGo Gophers slack channel one of the maintainers suggested to compile the sections separately and link them using a CGo flag: #cgo LDFLAGS: somefile.o. But after searching more for a solution, I found Wabin, which allowed me to programmatically add the 2 custom sections to the compiled module.

Here is a gist containing my code doing so:

Finally! I had the sections added and I was ready to invoke the add function using the soroban-cli.

Inspecting the contract

The soroban-cli offers an inspect functionality that inspects a WASM file listing contract functions, meta, etc.

I tried that first and here is the result:
soroban inspect --wasm add.wasm

File: add.wasm
• Interface Version: 27
• Function: add ([ScSpecFunctionInputV0 { name: StringM(x), type_: U32 }, ScSpecFunctionInputV0 { name: StringM(y), type_: U32 }]) -> ([U32])
Enter fullscreen mode Exit fullscreen mode

Looks good! :)

Invoking add


soroban invoke --wasm add.wasm --id 1 --fn add --arg 2 --arg 3


error: HostError
Value: Status(VmError(Validation))

Debug events (newest first):
0: "Validation"

Backtrace (newest first):
0: backtrace::capture::Backtrace::new_unresolved
1: soroban_env_host::host::err_helper::<impl soroban_env_host::host::Host>::err
2: soroban_env_host::vm::Vm::new
3: soroban_env_host::host::Host::call_n
4: soroban_env_host::host::Host::invoke_function
5: soroban::invoke::Cmd::run_in_sandbox
6: soroban::run::{{closure}}
7: <core::future::from_generator::GenFuture<T> as core::future::future::Future>::poll
8: tokio::runtime::park::CachedParkThread::block_on
9: tokio::runtime::scheduler::multi_thread::MultiThread::block_on
10: tokio::runtime::runtime::Runtime::block_on
11: soroban::main
Enter fullscreen mode Exit fullscreen mode

Oh no, no, no! Now go and find the reason! :)

0: backtrace::capture::Backtrace::new_unresolved

is this line of code in the soroban-env-host rust code (in soroban-env-host/src/host/

let  backtrace  =  backtrace::Backtrace::new_unresolved();
Enter fullscreen mode Exit fullscreen mode

When I had managed to build soroban-env, write a test and try to debug it, I was tortured by the thought that I have to get rid of the strange logic and the malloc like exports from the module.

But -gc=none didn't work, so I decided to ask in the TinyGo Gophers slack channel again. After some discussion, a maintainer of TinyGo pointed out a current pull request that aims to get rid of that exports and their logic (which were exported by accident).

Ok, sounds good, but when will this be available? Probably in the next version. But I wanted it now, so I decided to build TinyGo from that branch and try again.

Building TinyGo from the wasm-no-malloc branch

On the TinyGo website there is a description of how to build TinyGo for development.

With the new built version I tried it again:

Building the new wasm file


.../build/tinygo build -o add.wasm -target=wasm -wasm-abi=generic -scheduler=none --no-debug -panic=trap -opt z -gc=none add.go


-rwxr-xr-x 1 chris staff 456 7 Feb 16:08 add.wasm


and now optimize with wasm-opt:

wasm-opt -Oz add.wasm -o add.wasm


-rwxr-xr-x 1 chris staff 313 7 Feb 16:10 add.wasm

even better, 313 bytes and we started with 56506! See the new wat file.

Next, add the custom sections to the module and try again with the soroban-cli

soroban invoke --wasm add.wasm --id 1 --fn add --arg 2 --arg 3



And same on futurenet:


We can now build contracts with TinyGo and start experimenting. In the upcoming version of TinyGo - 0.27.0 - the pull request will hopefully be merged, so we don't need to build TinyGo from the wasm-no-malloc branch.

There is still a long way to go, but a first proof of concept is here. Maybe this will end up in a new Soroban SDK for TinyGo ;)

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.