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.
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:
import:
cp -a $(GOROOT)/src/go/parser/* .
cp -a $(GOROOT)/src/go/printer/nodes.go nodes.go.srcThe 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:
// 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.
2. Execution architecture
The performance hierarchy
Language runtimes fall along a well-understood performance spectrum:
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:
-
Pop from the Ops stack (slice bounds check, counter decrement)
-
Match against the switch cases
-
Call
incrCPU()for gas metering (overflow-safe multiply, comparison, accumulator update) -
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 -
Repeat for the right operand
-
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:
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 = 100These 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:
-
Recursive reference-count adjustment for created and deleted objects
-
Escape analysis for objects that crossed realm boundaries
-
Ownership tree traversal to mark dirty ancestors
-
Amino serialization of each modified object (reflection-based, no code generation)
-
SHA256 hashing for Merkle state commitment
-
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 |
|
Channels |
Inter-goroutine communication |
Not implemented |
Select |
Multiplexed channel operations |
Not implemented |
Complex numbers |
Numeric type |
|
Map iteration order |
Intentionally randomized |
Must be overridden |
Hardware floating point |
IEEE 754 arithmetic |
Software emulation, 10-100x slower |
|
Memory manipulation |
Blocked |
|
Runtime type introspection |
Not available |
|
Concurrency primitives |
Not available |
|
Cancellation and deadlines |
Not available |
|
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.
panic("not permitted")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:
-
Implementation independence. Ethereum has production clients in Go, Rust, C++, Java, and C#. The VM specification stands on its own.
-
Language evolution. Solidity, Vyper, and Fe compile to the same bytecode. Competition at the language layer improves the whole ecosystem without touching the VM.
-
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.