On the distance between Go and what runs on-chain.


Gno.land offers a compelling premise: smart contracts written in Go. The GnoVM is an interpreted virtual machine that executes a Go-derived language on-chain, with source-level transparency and composable package imports. The project carries significant pedigree from the Cosmos ecosystem and has attracted attention accordingly.

The appeal is clear. Go powers geth, Cosmos SDK, and Tendermint. It is the dominant language in blockchain infrastructure, and a smart contract environment native to Go could tap into a large and skilled developer base.

What follows is drawn directly from the source code.

Claim confidence Documented
SpeculativeArguedEvidencedDocumented
All claims derived from reading the gnolang/gno source code. Test provenance, interpreter architecture, stdlib coverage, and benchmarks are verifiable in the public repository.

1. Provenance

The test suite

The initial commit (January 29, 2021) included 754 test files in tests/files/. A tests/README.md accompanied them:

All the files in this directory are meant to be those derived/borrowed from Yaegi, Apache2.0.

Yaegi is a mature Go interpreter maintained by Traefik Labs under the Apache 2.0 license.

A file-by-file comparison bears this out. Of the 754 test files, 724 (96%) share identical filenames with Yaegi’s _test/ directory. The content is byte-for-byte identical across files like a1.go, struct1.go, closure1.go, issue-784.go, and hundreds more. All nine issue-*.go files reference Yaegi’s GitHub issue tracker, not Gno’s.

During a 2023 repo reorganization, tests/README.md was replaced with a new version that contains no reference to Yaegi. The borrowed test files remain in the codebase, many converted to .gno format, without attribution.

Matched test files (sample)

The following test files appear in both the Yaegi _test/ directory and Gno’s tests/files/, with identical content: a1.go, struct1.go, closure1.go, issue-784.go, issue-887.go, issue-1330.go, and hundreds more. All nine issue-*.go files reference Yaegi’s GitHub issue tracker, not Gno’s.

The parser

Gno’s parser (gnovm/pkg/parser/) is constructed by copying Go’s standard library parser and patching it. The Makefile is explicit:

Makefile
import:
    cp -a $(GOROOT)/src/go/parser/* .
    cp -a $(GOROOT)/src/go/printer/nodes.go nodes.go.src

The build process: copy go/parser wholesale, apply gno.patch, ship it.

Standard libraries

Gno ships roughly two dozen standard library packages, compared to Go’s 150+. Many are direct ports with provenance noted in headers:

  • gnovm/stdlibs/unicode/README.md: "Directly copied from go1.17.5."

  • gnovm/stdlibs/crypto/cipher/README.md: "Just the interfaces ported from https://pkg.go.dev/crypto/cipher@go1.17.5"

  • gnovm/stdlibs/regexp/regexp.gno: "Copyright 2009 The Go Authors."

  • gnovm/pkg/doc/dirs.go: "Mostly copied from go source at tip, commit d922c0a."

Floating-point arithmetic is handled by software emulation generated from Go’s own runtime:

Text
// This file is copied from $GOROOT/src/runtime/softfloat64.go.
// It is the software floating point implementation used by the Go runtime.

Consensus and serialization

The tm2/ directory contains Tendermint2, a fork of Tendermint Core. The Amino serialization library carries the header: "Original: Copyright © 2015 All in Bits, Inc; dba Tendermint Inc; Apache2.0."

What is original

The GnoVM interpreter itself, spanning tens of thousands of lines across nodes.go, values.go, types.go, preprocess.go, machine.go, ownership.go, and realm.go, is the original engineering. It defines a custom AST, a stack-based opcode dispatch loop, a bespoke type system, an ownership model for persistent objects, and realm-based state management. This design is architecturally distinct from Yaegi, which uses go/ast directly and compiles to a control flow graph.

The novel ideas live here: on-chain package imports, persistent object ownership, source-level deployment. Having worked with this code daily, I can confirm it is real and substantive work. But it sits on a foundation that is largely assembled from existing open-source components, and that assembly is not always clearly attributed in the current codebase.

96%
Test files from Yaegi
724 of 754 byte-identical
cp -a
Parser source
Go stdlib copied via Makefile
~24
Stdlib packages
Go has 150+

2. Execution architecture

The performance hierarchy

Language runtimes fall along a well-understood performance spectrum:

Native compilation (Rust, C, Go) Baseline
JIT compilation (Java HotSpot, V8) 2–5x overhead
Bytecode interpretation (EVM, WASM) 10–50x overhead
AST-walking interpretation (GnoVM) 50–200x overhead

GnoVM sits at the bottom of the performance hierarchy. Source code is parsed into a custom AST, and the interpreter walks that tree node by node at execution time. There is no compilation to bytecode. Every operation, whether addition, comparison, or function call, is dispatched through Go function calls, interface type assertions, and slice operations on six internal stacks.

Inside the hot path

The main loop in machine.go dispatches through a large switch statement covering roughly a hundred opcode cases. A straightforward operation like a + b traverses the following path:

  1. Pop from the Ops stack (slice bounds check, counter decrement)

  2. Match against the switch cases

  3. Call incrCPU() for gas metering (overflow-safe multiply, comparison, accumulator update)

  4. Call doOpEval() for the left operand: peek the Exprs stack, perform a Go interface type assertion, walk the block scope chain, dereference a pointer, push onto the Values stack

  5. Repeat for the right operand

  6. Call doOpAdd(): pop two values, switch on type kind, perform addition, push result

One integer addition: approximately 20 Go function calls, multiple interface type assertions, several slice operations, and two gas metering passes. In compiled Go, a + b is a single CPU instruction. Working on Playground and Studio, we felt this overhead in every realm deployment and test cycle.

Gas costs are provisional

Gas metering is the mechanism that prevents denial-of-service on a public network. In GnoVM, several gas constants are explicitly marked as unvalidated:

Go
OpCPUEnterCrossing       = 100 // XXX
OpCPUCallNativeBody      = 424 // Todo benchmark this properly
OpCPUReturnAfterCopy     = 38  // XXX
// TODO: OpCPUStaticTypeOf is an arbitrary number.
// A good way to benchmark this is yet to be determined.
OpCPUStaticTypeOf        = 100

These are not placeholders in dead code. They are the values that validators depend on to bound resource consumption per transaction. Mispriced gas creates an attack surface: transactions that consume disproportionate compute relative to their fee.

Note
The XXX and TODO markers in gas constants are present in the master branch at the time of writing. In a production network, unvalidated gas pricing is a direct vector for resource exhaustion attacks.

The finalization tax

Every state-modifying transaction triggers FinalizeRealmTransaction (realm.go), which performs:

  1. Recursive reference-count adjustment for created and deleted objects

  2. Escape analysis for objects that crossed realm boundaries

  3. Ownership tree traversal to mark dirty ancestors

  4. Amino serialization of each modified object (reflection-based, no code generation)

  5. SHA256 hashing for Merkle state commitment

  6. IAVL tree insertion (O(log n) per write)

A simple token transfer modifies at least two balance objects. Each goes through the full serialize-hash-persist pipeline. This cost is structural. It is inherent to the ownership model and runs on every transaction that touches state.

Consensus as ceiling

Even setting VM performance aside, Tendermint2’s three-phase BFT commit (propose, prevote, precommit) imposes its own throughput limit. Production Cosmos chains on the same consensus architecture typically achieve 20-100 TPS. VM execution time stacks on top of consensus time.

The rest of the industry is heading in the opposite direction, toward compiled execution. Solana's BPF, Sui's Move VM, Monad's parallel EVM, and Ethereum’s EOF proposals all prioritize raw execution throughput. GnoVM’s interpreted architecture has no clear path to compete on this axis.


3. The language gap

The value proposition hinges on familiarity: Go developers writing smart contracts in a language they already know. In practice, the gap between Gno and Go is significant.

What is stripped

Deterministic execution across all nodes is a hard requirement for any blockchain VM. Go was not built for this. Several of its defining features are non-deterministic by nature, and GnoVM removes them:

Feature Role in Go Status in Gno

Goroutines

Concurrent execution

panic("goroutines are not permitted")

Channels

Inter-goroutine communication

Not implemented

Select

Multiplexed channel operations

Not implemented

Complex numbers

Numeric type

panic("imaginaries are not supported")

Map iteration order

Intentionally randomized

Must be overridden

Hardware floating point

IEEE 754 arithmetic

Software emulation, 10-100x slower

unsafe

Memory manipulation

Blocked

reflect

Runtime type introspection

Not available

sync

Concurrency primitives

Not available

context

Cancellation and deadlines

Not available

os, net, io/fs

System interaction

Not available

Go has over 150 standard library packages. Gno has roughly two dozen. Goroutines, the signature feature and often the primary reason developers choose Go, are gone entirely.

Go vs Gno: what the syntax preserves and the runtime removes
Go
Gno
Goroutines
Signature concurrency feature
panic("not permitted")
Channels
Inter-goroutine communication
Not implemented
Stdlib
150+ packages
~24 packages
Floating point
Hardware IEEE 754
Software emulation, 10-100x slower
Execution
Compiled to native code
AST-walking, 50-200x overhead
unsafe, reflect, sync
Available
Blocked

The syntax is familiar.
The guarantees are not.

What remains is a language that uses Go’s syntax and a fraction of its semantics. That is a meaningful distinction.

Failures at runtime, not compile time

The VM source contains dozens of panic("not yet implemented") calls scattered across core files including machine.go, uverse.go, gonative.go, op_expressions.go, values_conversions.go, and values_fill.go. A developer writes code that is valid Go, the code passes parsing and preprocessing, and then it panics during execution when it hits an unimplemented path. Building realm contracts for Boards and CommonDAO surfaces this issue: patterns that feel natural in Go produce panics deep in the VM.

For a smart contract platform handling real value, runtime panics on syntactically valid input are a serious concern.

Gno-specific concepts

Effective Gno development requires understanding constructs that do not exist in Go:

  • Realms: persistent packages whose state survives across transactions

  • crossing(): the call pattern that gates cross-realm interaction

  • Ownership semantics: objects belong to realms, tracked and serialized by the runtime

  • Import restrictions: only on-chain packages and a small subset of ported standard library packages are available

The resulting learning curve is comparable to picking up a purpose-built smart contract language, but without the tooling maturity that comes with established platforms like Solidity or Move. Building the developer experience tools around Gno, we spent as much time explaining what Gno is not as what it is.


4. Why the industry chose differently

Ethereum’s node software is written in Go. Cosmos SDK is written in Go. Tendermint is written in Go. None chose Go as a smart contract language. Understanding why is essential context.

The separation principle

Ethereum’s design rationale describes a "sandwich complexity model": simple at the bottom (VM), simple at the top (language), complex in the middle (compilers). This separation delivers three concrete advantages:

  1. Implementation independence. Ethereum has production clients in Go, Rust, C++, Java, and C#. The VM specification stands on its own.

  2. Language evolution. Solidity, Vyper, and Fe compile to the same bytecode. Competition at the language layer improves the whole ecosystem without touching the VM.

  3. Verification tractability. Formally verifying a 200-opcode instruction set is tractable. Formally verifying an AST-walking interpreter that covers a general-purpose language surface is a different order of problem.

GnoVM collapses this separation. The VM is the language. It cannot be reimplemented in Rust without reimplementing Go’s runtime semantics. The language cannot evolve independently of the interpreter. And formal verification must reason about the full surface of Go minus an incompletely documented set of exclusions.

The audit problem

Solidity’s type system is intentionally minimal: roughly 30 keywords, a constrained type system, no pointer arithmetic, no dynamic dispatch beyond interfaces. This makes contracts auditable and formal verification practical.

GnoVM exposes Go’s full type surface (structs, interfaces, closures, defer, panic/recover, type assertions, slices, maps, pointers, methods, embedding) bounded by a type checker and preprocessor that lives in a single, sprawling file: preprocess.go. This monolith handles name resolution, type inference, constant evaluation, static analysis, heap escape analysis, and AST transformation, all in thousands of lines of deeply nested logic.

Any bug in this file is a potential consensus vulnerability. If two nodes type-check the same contract differently, the chain forks. Auditing a monolith of this scale for correctness is qualitatively harder than auditing a compact bytecode specification. This is not a theoretical concern; it is the central security challenge of the architecture.

Warning
A single bug in the type checker or preprocessor can cause determinism failures across validators, leading to chain halts or forks. The monolithic structure of preprocess.go makes targeted auditing extremely difficult.

Tooling

Solidity has eight years of production deployment, roughly 9,000 monthly active developers, and a deep tooling ecosystem: Hardhat, Foundry, Slither, Mythril, Certora, and multiple formal verification frameworks. Move has linear types with built-in safety guarantees. Rust on Solana inherits cargo and the broader Rust ecosystem.

Gno has none of these. No production-grade static analyzer. No fuzzer. No formal verification framework. No battle-tested deployment pipeline. Having built Gno Studio and Playground to try to bridge this gap, I can say the tooling challenge is not for lack of effort. The architecture itself makes it difficult. The VM’s surface area is large, the behavior is underspecified in places, and the absence of a bytecode layer means tooling must reason about the full interpreter. For a platform intended to secure real value, this is not simply a matter of maturity. It is a matter of readiness.


5. What is genuinely novel

Three ideas in GnoVM deserve recognition on their own merits:

On-chain package imports. Writing import "gno.land/p/demo/avl" and having it resolve to deployed, on-chain source code is a genuine composability innovation. It creates a dependency graph between contracts that mirrors conventional software development, more natural than Solidity’s interface-and-address indirection.

Source transparency. Contracts deploy as source code, readable by anyone without decompilation. Ethereum approximates this through Etherscan verification, but Gno makes it structural. Transparency is a property of the system, not an opt-in service.

Persistent object ownership. The realm/ownership model provides typed, structured on-chain state management that goes beyond raw key-value stores. The design is thoughtful, even if the runtime cost is substantial.

These are ideas worth building on. Notably, they are also separable from the interpreter architecture. On-chain imports and source transparency could be implemented on an existing high-performance VM without the overhead of AST-walking or the constraints of a restricted Go subset.


Conclusion

Gno.land positions itself as Go for blockchain. The codebase tells a more nuanced story.

The project is a composite. Its test suite originates from Yaegi. Its parser is copied from Go’s standard library. Its standard libraries are ported from Go at varying levels of completeness. Its consensus layer is forked from Tendermint. Its serialization is forked from Amino. The original contribution, the GnoVM interpreter, its type system, and its persistence model, is genuine and contains ideas of real value.

But the interpreter uses AST-walking, the slowest mainstream execution model. The consensus layer inherits Tendermint’s throughput constraints. Gas metering costs are partially unvalidated. The type checker is a sprawling monolith with direct implications for consensus safety. And the language is not Go. It is a restricted subset missing goroutines, channels, select, complex numbers, reflection, and the vast majority of the standard library, with dozens of panic("not yet implemented") paths still live in the VM.

The blockchain industry has already explored the question of whether smart contracts should use general-purpose language syntax. Ethereum chose purpose-built languages compiled to simple bytecode. Solana chose compiled-to-native execution. Sui and Aptos chose Move, engineered from scratch for on-chain safety and formal verification. Each traded syntax familiarity for execution speed, reduced audit surface, and tooling maturity.

Gno made the opposite trade. The result is a system that executes slower than the EVM, implements less of Go than Go, offers less tooling than Solidity, and presents a larger audit surface than any of its peers, all in exchange for the familiarity of syntax that, on closer inspection, is not quite the language it resembles.

The novel ideas are real. I have seen them work. Whether they require this particular architecture to exist is the open question.

The syntax is familiar.
The language is not.