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
}
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
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
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)
...
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:
addsec.go
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
Env Meta: AAAAAAAAAAAAAAAb
โข Interface Version: 27
Contract Spec: AAAAAAAAAANhZGQAAAAAAgAAAAF4AAAAAAAAAQAAAAF5AAAAAAAAAQAAAAEAAAAB
โข Function: add ([ScSpecFunctionInputV0 { name: StringM(x), type_: U32 }, ScSpecFunctionInputV0 { name: StringM(y), type_: U32 }]) -> ([U32])
Looks good! :)
Invoking add
Command:
soroban invoke --wasm add.wasm --id 1 --fn add --arg 2 --arg 3
Result:
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
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/errors.rs):
let backtrace = backtrace::Backtrace::new_unresolved();
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
Command:
.../build/tinygo build -o add.wasm -target=wasm -wasm-abi=generic -scheduler=none --no-debug -panic=trap -opt z -gc=none add.go
Result:
-rwxr-xr-x 1 chris staff 456 7 Feb 16:08 add.wasm
cool!
and now optimize with wasm-opt
:
wasm-opt -Oz add.wasm -o add.wasm
Result:
-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
Result:
5
Tadaaa!
Conclusion
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)