The realm keeps a small append-only log in a Merkle mountain range and hands out inclusion
proofs against its root. Appending costs O(log n) hashes and never rebuilds. Verification is
a single native crypto/merkle call.
Append("something") → index
Root() → the current root, hex
ProofOf(index) → path, before, after
Verify(root, index, total, entry, path, before, after) → bool
Verify takes the root as an argument rather than reading it from state. That is the
honest signature: the realm is a verifier, not an oracle. Passing Root() checks against the
live log; passing an older root checks against that older log.
VerifyFixed accepts the other proof encoding, the flat Tendermint sibling list, against the
same root. Both work because the log's root is the Tendermint simple-tree root over the
same entries. TestVerifyFixedAcceptsTendermintProof pins that rather than asserting it.
The half that is the point
A proof from this realm says this entry is consistent with the root this realm published.
It cannot say this root is the chain's own. Two independent reasons, both verifiable in the
monorepo source:
1. Realm state is not merkleized.gno.land/pkg/gnoland/app.go mounts exactly two stores:
1baseApp.MountStoreWithDB(mainKey,iavl.StoreConstructor,cfg.DB)// merkleized2baseApp.MountStoreWithDB(baseKey,dbadapter.StoreConstructor,cfg.DB)// not
and tm2/pkg/store/dbadapter/store.go says it in a comment: "Always returns a zero
commitID, as dbadapter store doesn't merkleize". The VM keeper takes both, and
gnovm/pkg/gnolang/store.go puts realm objects, types and realm metadata in the base
store.
Data
Store
Provable against the app hash
Package source
iavl
yes
Account balances
iavl
yes
Escaped object hashes (cross-realm, refcount > 1)
iavl
yes, the hash only
Realm objects, types, realm metadata
base
no
This log
base
no
So you can prove to a light client that a package has a given source, and that an address
holds a given balance. You cannot prove that any realm's AVL tree contains any key. Every
GRC20 balance and every DAO vote on the chain is, today, unprovable.
2. A realm cannot see a header.chain/runtime exposes ChainID, ChainDomain,
ChainHeight and GetSessionInfo, and nothing else. No app hash, no block hash, no header.
Even handed a valid Tendermint proof, a realm has no trusted root to check it against.
The one crack in the wall: an escaped object, one referenced across realm boundaries, does
get its hash written into the IAVL store. Cross-realm objects are partially provable already.
Nothing else is.
Why say so out loud
Merkle proofs are the part of a chain that most invites overclaiming. "Verifiable on chain"
is true here in a narrow sense and false in the sense a reader assumes. Anything built on
Merkle proofs in a gno realm is trust-minimised relative to a committed root, never trustless.
A demo that showed only the working half would be the interesting-sounding part of a true
story.
Bounds
MaxEntries is 512 and MaxEntryLen is 256 bytes. Render lists the last 10 entries, so the
page stays a fixed size: the chain caps a query at maxGasQuery and a reader cannot raise it,
so an unbounded Render is a permanently unreadable page.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
Dependency graph:
🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.
Overview
Package provable is an honest demonstration of what gno.land can and cannot prove about itself.
It keeps a small append-only log in a Merkle mountain range and hands out inclusion proofs against the log's root. That part works, and it is the useful half: a realm can commit to a set and let anyone verify membership cheaply, with one native call.
The other half is the point of the realm. A proof here says "this entry is consistent with the root this realm published". It cannot say "this root is the chain's own", because:
gno.land mounts two stores (gno.land/pkg/gnoland/app.go). The IAVL one is merkleized and holds package source and account balances. The other is a plain dbadapter whose Commit is documented as "Always returns a zero commitID, as dbadapter store doesn't merkleize", and THAT is where every realm's objects live. So no realm's state, including this log, contributes anything to the app hash. There is no state proof to produce.
chain/runtime exposes ChainID, ChainDomain, ChainHeight and GetSessionInfo, and nothing else. No app hash, no block hash, no header. So even given a Tendermint proof, a realm has no trusted root to check it against.
The one crack in the wall: an ESCAPED object, one referenced across realm boundaries, does get its hash written into the IAVL store (gnovm/pkg/gnolang/store.go). Cross-realm objects are therefore partially provable already. Nothing else is.
Demo of gno.land/p/moul/x/merkle/v0 and gno.land/p/moul/x/mmr/v0.
1const( 2// MaxEntries bounds the log. Unbounded append from an untrusted caller is 3// a storage-growth hazard even when the caller pays the deposit, and an 4// unbounded Render is a permanently unreadable page: the chain caps a 5// query at maxGasQuery, and a reader cannot raise it. 6MaxEntries=512 7 8// MaxEntryLen bounds one entry. 9MaxEntryLen=2561011// renderEntries is how many of the most recent entries Render lists, so12// the page stays a fixed size no matter how full the log is.13renderEntries=1014)
Append adds an entry to the log and returns its index. The root moves, so every previously issued proof stops verifying against the new root: that is the nature of an append-only commitment, not a bug.
Render serves the overview at "" and one entry with its proof at "entry/<index>".
Output is a fixed size: the entry list is capped at renderEntries, so a full log still renders. The chain caps a query at maxGasQuery and a reader cannot raise it, so an unbounded Render is a permanently unreadable page.
Root returns the current log root, hex-encoded. It is also the Tendermint simple-tree root over the same entries, so any Tendermint verifier accepts it.
Verify checks a proof against a root the caller supplies, which is the honest signature: the realm is a verifier, not an oracle. Pass Root() to check against the live log.
VerifyFixed checks a Tendermint simple-tree proof, the fixed-leaf-set encoding, against a root the caller supplies. Same tree as the log, a different proof shape: siblings are one flat list, leaf first.
Both encodings verify against the same root, because the Tendermint tree and a right-bagged mountain range are the same structure.