Written by Joseph Toba for Tanjira Validator
Gno.land’s entire chain binary has four keepers. Its validator set, its governance, its fees and its IBC stack are not in there. They are smart contracts, and the chain reads them.
Gno.land has no staking module.
It has no governance module either. No distribution module, no slashing module, no IBC module. If you open the chain’s application code and count the keepers, the pieces of Go that own a chunk of consensus state, you find four.
That is the entire chain. For comparison, a normal Cosmos SDK chain runs fifteen to twenty modules. Staking, slashing, distribution, governance, evidence, IBC, transfer, interchain accounts, feegrant, authz, each one compiled into the binary, each one requiring a coordinated network upgrade to change.
And yet gno.land has validators. It has governance. It has a name registry, a user registry, transaction fees, a fee collector, and a working IBC connection to another chain. All of the things those missing modules would provide.
So where are they?
Every one of them lives in userland, as a realm, which is what gno.land calls a stateful smart contract.
The validator set is gno.land/r/sys/validators/v3. Governance is
gno.land/r/gov/dao. Chain parameters are gno.land/r/sys/params.
Transaction fees are gno.land/r/sys/txfees. Rewards are
gno.land/r/sys/rewards. The name registry is
gno.land/r/sys/names. Users are gno.land/r/sys/users. Even the
chain’s IBC implementation, the thing that lets it talk to other blockchains, is a
contract: gno.land/r/aib/ibc/core.
None of these are modules pretending to be contracts. They are ordinary Gno code, deployed at ordinary package paths, callable by anyone, and readable by anyone as source. You can go and read the code that decides who validates this blockchain, in the same way you would read the code of a token or a game.
That is a genuinely unusual architecture, and the first time you notice it the obvious question is how it can possibly work. Consensus cannot ask a smart contract for permission to produce a block. The validator set has to exist before the VM does.
The answer is in how the two halves talk to each other, and it is worth walking through, because this is where the design stops being a curiosity and starts being clever.
The validators realm documents its own mechanism at the top of the file, and the description is better than any paraphrase:
“The realm exposes a public proposal constructor for GovDAO; on approval, the proposal callback applies the captured deltas to the chain’s effective valset and publishes the new full set via gno.land/r/sys/params. The chain’s EndBlocker reads the result on the next block and propagates to consensus.”
examples/gno.land/r/sys/validators/v3/validators.gno
So the flow runs contract first, chain second. GovDAO, itself a contract, approves a
proposal. The validators realm computes the resulting set and writes it into the params
store. A dirty flag gets set. At the end of the next block, the chain’s
EndBlocker notices the flag, reads the proposed set, parses it, and hands it
to consensus as an ABCI validator update.
The Go binary does not decide anything here. It is a courier. The decision was made in a smart contract, and the chain’s job is to notice and comply.
There is a detail in the validators realm worth pausing on, because it shows how seriously the separation is taken:
// No in-realm validator state. All reads go through
// sysparams.GetValsetEffective (proposed-if-dirty, else current).
The contract does not even keep its own copy. There is one source of truth, in the params store, and both the contract and the chain read from it. No drift, no reconciliation, no two versions of who the validators are.
The same EndBlocker holds one more line worth knowing:
// Check if GovDAO has requested a halt at this height.
Governance, a contract, can set a halt height, and the chain will stop itself there, deterministically, on every node at once. Coordinating that on most chains means operators agreeing out of band and everyone running the same command at roughly the same time. Here it is one governance proposal. The chain does what its contracts tell it, including stopping.
The validator set is one contract making a decision the chain trusts. There is a harder case worth looking at, because it tests the same idea against the part of a blockchain everyone agrees should never be improvised: verifying that another chain is telling the truth.
Gno.land talks to other chains over IBC, and the realm doing it,
gno.land/r/aib/ibc/core, imports a package called
gno.land/p/aib/ibc/lightclient/tendermint. It is deployed and live on Pearl
today, maintained separately from the core gnolang/gno repository, and its
source is readable from gno.land’s own export of what is actually running on-chain.
Open it and it is not a wrapper around someone else’s verifier. It is the verifier:
trust-level thresholds, header expiry, clock-drift checks, adjacent and non-adjacent
commit logic, and at the center of it, this line.
if !ed25519.Verify(val.PubKey, voteSignBytes, commitSig.Signature) {
return ufmt.Errorf("invalid signature")
}
That is a real signature check, against a real validator’s public key, over data a
real blockchain signed, written in Gno and calling Go’s own crypto/ed25519
standard library directly. On almost every chain in Cosmos, this exact logic is the one
piece nobody would dream of putting in application code, because a bug here does not
corrupt a token balance, it forges consensus. Gno.land put it in a package anyone can
import.
And it is not the only bridge running. A second, independent stack called Union
connects gno.land to Ethereum, and it does not speak Cosmos’ usual event format at
all. Its packets are ABI-encoded, Ethereum’s own binary wire format, decoded by hand
against a real transaction to confirm it: a gno-native asset on one side, an Ethereum
address on the other, chain id 11155111, which is Sepolia’s actual
EIP-155 identifier, not a placeholder. Wrapped Sepolia ETH and a bridged USDT have both
moved across it as real, if small, testnet balances.
That stack went live on topaz, gno.land’s previous testnet, and was
already live at genesis on the one after it, sapphire. On pearl,
the current chain, it has not been redeployed yet. Given that every other piece of this
ecosystem came back within days of the last three resets, that gap looks temporary rather
than abandoned.
Three reasons, and they compound.
You can read it. Gno.land publishes contract source, not bytecode. A deployed realm is stored as the code its author actually wrote. There is no compile step to trust, no verification service to believe, no “this bytecode allegedly came from that repository.” When the validator set logic lives in a realm, auditing it means reading it. Compare that to auditing a staking module, which means finding the right binary, checking it matches the tag, and trusting the build.
You can change it without a hard fork. A Go module lives in the binary.
Changing it means a new release, a governance proposal, an upgrade height, and every
validator restarting in a coordinated window. Changing a realm means deploying a new realm
and pointing governance at it. The version suffixes in these paths,
validators/v3, boards2/v1, gov/dao/v3, are not
cosmetic. They are the upgrade history, visible on chain, with the old versions still
sitting there readable.
You can fork it. Anything readable and deployable is also copyable. If you dislike how the name registry works, the code is right there and nothing stops you deploying your own. The system realms are only systemic because governance points at them, not because the chain privileges their code.
Forking is what you reach for when you disagree with something and want your own version. There is a quieter, far more common kind of reuse sitting right next to it: importing someone else’s code as a dependency instead of writing your own, and gno.land’s ecosystem runs on it.
Almost every token on the chain, regardless of who deployed it, is built on one shared
package: gno.land/p/demo/tokens/grc20. Its own documentation shows the entire
interface in one line:
Token, ledger := grc20.NewToken("Foo", "FOO", 4, nextTokenID.Next(), cur)
That single call hands back a token and a private ledger, with minting, balances, transfers and allowances already implemented, tested, and unforgeable by construction, the token’s identity is bound to whichever realm created it, so nothing else can spoof it. Nobody importing it has to write that logic themselves, and nobody using their token has to take their word that they got it right.
What stands out is not that the package exists. It is who reaches for it. GnoSwap’s
own governance token and its staked-voting token both import it, confirmed in the deployed
source on three separate testnets in a row: Topaz, Sapphire, and Pearl. So does the
aib IBC realm from earlier in this piece, the one doing raw Ed25519 signature
verification against another blockchain’s validators. When it needs to represent a
bridged voucher as a spendable token, it does not write its own token code either. It
imports the same package everyone else does. Onbloc’s token utilities import it.
Samcrew’s Memba marketplace imports it for payments. Independent contributor
moul’s demo apps import it. A long tail of one-off tokens, deployed straight from
ordinary wallet addresses with names like pearl_token and testtoken,
import it too.
None of these teams coordinated with each other. Most of them have no reason to know the others exist. What they share is a package path, and that is enough. A dependency that gets pulled into a DEX’s governance token, a cross-chain bridge, a marketplace, and a stranger’s weekend project without any of them asking permission from the others is reuse in the sense that actually matters: not a right you could theoretically exercise, but one that quietly runs.
Render() returns markdown. Markdown can contain an image. The obvious
question is where that image actually lives, and gno.land’s web frontend answers it
with a policy narrow enough to be a real decision rather than an oversight, found directly
in the code that enforces it:
“
gno.land/pkg/gnoweb/markdown/ext_imgvalidator.goAllowSvgDataImageis gnoweb’s image policy: everydata:URI is rejected exceptimage/svg+xml.”
An ordinary link still works for anything, PNG on IPFS, a photo on a normal server. But a
realm can also embed the entire image inline, as a self-contained data: URI,
and gno.land’s renderer will display it with nothing external to fetch, nothing to
pin, nothing that can go offline. It only works for one format: SVG. Every raster
equivalent is explicitly blocked at the same check.
That is a narrower claim than “gno.land replaces IPFS,” and a more honest one. It replaces IPFS specifically for generative, vector art, which turns out to be most of what an on-chain NFT actually wants to be.
GnoSwap’s own NFT is the clearest live example. Every liquidity position on the exchange is represented by an NFT, the same idea as Uniswap’s position tokens on Ethereum, and its image is generated on demand rather than stored as a file:
// MUST BE IMMUTABLE, DO NOT MODIFY.
// SVG template structure: ...
return "data:image/svg+xml;base64," + sEnc
That comment is doing real work. The template is treated the way an audited constant is
treated elsewhere in this piece, frozen on purpose, because it is rendered fresh on every
view rather than written once and forgotten. The realm imports
gno.land/p/gnoswap/deps/tokens/grc721 for the token standard itself, the same
reuse pattern as the shared GRC20 package, just one door over. And it is not a one-off: the
same import, the same generated-SVG pattern, is present in the deployed source on Topaz,
Sapphire, and Pearl in a row.
It is not the only example, either. A name-service visualizer from an independent deployer
has iterated through six versions, each with its own BuildSVG function,
surviving the reset from Sapphire to Pearl intact. And there is a general-purpose SVG
toolkit published as an ordinary importable package, Canvas,
Circle, Rectangle, Path, composable the way any
drawing library is composable, deployed the same way any other realm dependency is
deployed. Whether other teams have started pulling it in yet, I could not confirm. That it
exists as a plain package rather than a special chain feature is the point regardless.
There is a second thing that makes this work, and it is the part that surprised me most when I first read the documentation.
In Gno, state persistence is automatic.
“Realms are stateful, meaning they keep their state between transaction calls. In practice, global variables used in realms are automatically persisted after a transaction has been executed. Thanks to this, Gno developers do not need to bother with the intricacies of state management and persistence, like they do with other languages.”
docs/resources/realms.md
Declare a global variable. Change it in a function. It is still changed in the next transaction, next week, next year.
If you have written Solidity you have thought carefully about storage layout and slots. If you have written CosmWasm you have serialized structs in and out of a key-value store and paid attention to what that costs. In Gno you write a Go program and the state is just there.
That is why putting the validator set in a contract is reasonable rather than reckless. The contract is not a workaround around a hostile execution environment. It is a normal Go program whose variables happen to survive.
Two related distinctions worth knowing. Code under gno.land/p/ are pure
packages: immutable once deployed, no state, safe to import. Code under
gno.land/r/ are realms: stateful, callable, upgradeable by whoever governs
them. Libraries and applications, separated at the path level, with the immutability
guarantee enforced by the chain rather than by convention.
“The state is just there” understates something. It is there because somebody is paying for it to be there, and gno.land keeps that payment separate from ordinary gas in a way worth being precise about.
Every operation burns gas, non-refundable, proportional to the bytes it touches, same as any chain. That is the cost of the chain doing the work. Storing new data triggers a second, independent charge on top of it: a deposit.
“Storing data → GNOT locked. Deleting data → GNOT refunded.”
docs/resources/storage-deposit.md
The rate is a governed parameter, not a constant buried in the binary:
StoragePrice, currently 100ugnot per byte, so roughly 1 GNOT buys 10KB. Write
data and that much GNOT locks into an address the chain derives deterministically from the
realm’s own path. Delete the same data later and the deposit flows back out of that
address. Gas is gone the moment you spend it. A deposit is a loan the chain is holding, not
a fee it has kept.
The detail that makes this more than bookkeeping: deposits are tracked per realm, not per user. Whoever deletes unused data collects the refund, regardless of who originally paid it in. That turns cleanup into an incentive rather than a chore, a standing bounty for anyone willing to go find bloated, abandoned state and clear it out. Nobody has to be nagged into housekeeping. Somebody just has to notice it pays.
And StoragePrice itself is set the same way this whole piece keeps finding
things get decided here: not hardcoded, governed. Change what storage costs on gno.land and
you are proposing it the same way you would propose changing who validates.
I run an indexer for this chain, so rather than argue in the abstract, here is what the current testnet actually shows.
Pearl, the latest gno.land testnet, launched on the 27th of August as a completely fresh chain. Nothing carried over: zero balances, no deployed contracts, every registered name released. Nine days later:
Inside that, the system realms are not ornaments. The validator realm,
gno.land/r/sys/validators/v3, has been called 195 times by
87 distinct addresses, with the validator operator registry at
r/gnops/valopers showing an almost identical 87. That is eighty-seven separate
parties interacting with the validator set the way you would interact with any other
contract. On a normal Cosmos chain, becoming a validator is a
MsgCreateValidator to a module. Here it is a function call to code you can
read.
The user registry has 230 callers, the name registry 228. The IBC core realm has real
traffic on it. Governance is live at r/gov/dao with a member store and an
implementation realm beside it, each carrying its own version suffix, each one a contract
somebody can inspect.
And the ecosystem around them rebuilt itself from nothing in nine days, which it has now
done four times this summer as gno.land tore down and replaced its testnet:
test-13, then topaz, then sapphire, now
pearl. Every reset, the contracts come back within days. That happens because
redeploying a realm is just publishing code again.
There is a philosophical claim buried in this architecture, and I think it is the most interesting thing about the project.
Most blockchains draw a hard line between the chain and the applications on it. The chain is privileged, written in a systems language, changed by coordinated upgrade, audited by specialists. Applications sit on top in a sandbox, restricted, metered, and treated as untrusted by default.
Gno.land erases most of that line. The application language is good enough to write the system in, so the system is written in it. Validators, governance, naming, fees and interchain communication are not special. They are just the contracts that governance happens to point at, running in the same VM, readable by the same tools, upgradeable by the same process as a tic-tac-toe game deployed by a stranger.
That is a bet that a smart contract platform can be good enough to build a blockchain out of. It is a much bigger claim than “we support smart contracts,” and gno.land is one of very few chains actually making it.
The four keepers are the proof. Params, auth, bank, VM. Everything else is code you can read.