Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

v0 source realm

Package provable is an honest demonstration of what gno.land can and cannot prove about itself.

Readme View source

provable

What gno.land can and cannot prove about itself, demonstrated rather than asserted.

Demo realm for p/moul/x/merkle/v0 and p/moul/x/mmr/v0.

The working half

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)      // merkleized
2baseApp.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:

gno.land/r/moul/x/provable/v0 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:

  1. 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.
  2. 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.

Constants 1

const MaxEntries, MaxEntryLen, renderEntries

 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.
 6	MaxEntries = 512
 7
 8	// MaxEntryLen bounds one entry.
 9	MaxEntryLen = 256
10
11	// renderEntries is how many of the most recent entries Render lists, so
12	// the page stays a fixed size no matter how full the log is.
13	renderEntries = 10
14)
source

Functions 8

func Append

crossing Action
1func Append(cur realm, entry string) int
source

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.

func ProofOf

Action
1func ProofOf(index int) (path, before, after string)
source

ProofOf returns the inclusion proof for index, as the three comma-separated hex lists Verify expects.

func Render

1func Render(path string) string
source

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.

func Root

Action
1func Root() string
source

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.

func Verify

Action
1func Verify(root string, index, total int, entry, path, before, after string) bool
source

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.

func VerifyFixed

Action
1func VerifyFixed(root string, index, total int, leaf, siblings string) bool
source

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.

Imports 4

Source Files 6