.gnolooks like Go but runs in a deterministic VM.- No goroutines, channels,
unsafe,net,os. - Realms (
r/) are stateful (on-chain state). - Packages (
p/) are stateless libraries. - Import paths:
gno.land/p/...orgno.land/r/...— nevergithub.com/.... - Module config:
gnomod.toml(notgo.mod). crosskeyword for cross-realm calls.realmtype for realm-aware functions.
| Gno tests | Go tests |
|---|---|
gno test ./path/... |
go test ./... |
Test files: _test.gno |
Test files: _test.go |
- Commits: conventional (
feat:,fix:,docs:,chore:,test:,refactor:) with optional scope (feat(gnovm):). - Branches: kebab-case (
fix-vm-params), always feature branches. - If posting comments or reviews under your owner's GitHub account, disclose that you are an AI agent (e.g., prefix with
[bot]or add a "generated by" note) so humans can distinguish agent activity from the account owner. - Add
Assisted-By(NOT Co-Authored-By) lines or AI tool credits (e.g., "Generated by") in commits and PR descriptions. - Keep PRs up to date with
master— merge regularly and resolve conflicts promptly. - When merging
masterinto your branch, review incoming changes carefully. IfCONTRIBUTING.mdorAGENTS.mdwas updated, re-read and follow the latest rules. - Be respectful and helpful in PR comments and reviews — optimize for the reviewer's time, especially human maintainers.
Every non-trivial AI-assisted PR must include an ADR. This documents what the AI understood so reviewers can verify assumptions and future contributors can build on it.
| Scope | Directory |
|---|---|
| GnoVM | gnovm/adr/ |
| gno.land | gno.land/adr/ |
| Tendermint2 | tm2/adr/ |
Naming: pr<number>_<description>.md.
Use prxxxx_ if PR number unknown.
Include: context, decision, alternatives considered, consequences.
See gnovm/adr/ for examples.
Match detail to complexity.
Skip ADRs for: trivial bug fixes, formatting, simple tests, docs-only changes.
git push --force— never force push unless explicitly asked.--no-verify— never skip hooks.go generate— slow, large diffs. Only if explicitly asked.- Modify
gno.land/genesis/— only if that's the task. - Goroutines/OS calls in
.gno— never works. - Break
gno.land/p/demo/backwards-compat — needs discussion. - AI-assisted PRs without an ADR.
ref: https://github.com/allinbits/gno-realms/blob/master/AGENTS.md#gno-specific-conventions
Each package/realm has a gnomod.toml (not gno.mod):
module = "gno.land/r/gnoswap/ibc/apps/zkgm/v0/impl"
gno = "0.9"p/= packages (stateless, reusable libraries)r/= realms (stateful contracts with persistent storage)
Realm functions that need caller context use cur realm parameter:
func CreateClient(cur realm, clientState lightclient.ClientState, ...) string
Callers pass cross as the argument:
clientID := core.CreateClient(cross, clientState, consensusState)
A non-crossing function (func F() T) called from another realm runs in the caller's realm-storage-context. Persistent state owned by the function's home realm is reachable but treated as foreign:
- Scalar and string fields read fine — the value is copied.
- Slice, pointer, and map fields come back as readonly tainted references. Operations that touch the underlying object — reassignment,
string([]byte),bytes.Equal,copy,append, range-iteration that mutates — panic withcannot directly modify readonly tainted object.
Returned structs that contain such reference fields stay tainted on the caller's side, so deep-cloning inside the getter doesn't help: the caller can't write back into the local copy either.
Rule of thumb: any getter that exposes a reference-typed field from a realm's persistent state must be a crossing function (cur realm) and return immutable values — string, int, or freshly-allocated objects.
// BAD — runs in caller's realm; conversion of tainted []byte panics.
func GetPayload() string { return string(state.payload) }
// GOOD — runs in the owning realm; string crosses back safely.
func GetPayload(cur realm) string { return string(state.payload) }
Prefer one accessor per scalar field over a single struct-returning getter: GetX(cur realm) string, GetY(cur realm) int, etc.
bytes.Equal(a, b) is implemented as string(a) == string(b), so comparing against any cross-realm []byte (including package-level []byte constants imported from another realm or p/) panics. Use a manual loop when the operand may be foreign:
func equalBytes(a, b []byte) bool {
if len(a) != len(b) { return false }
for i := range a { if a[i] != b[i] { return false } }
return true
}
Most IBC functions require MsgRun (not MsgCall) because they take complex arguments (structs, slices of bytes). The IBC core realm itself lives at gno.land/r/aib/ibc/core (vendored from gno-realms); see filetests under gno.land/r/core/ibc/apps/zkgm/v0/impl/ for working MsgRun examples.
chain/banker- coin manipulation interfacechain.Emit(eventType, kvPairs...)- event emissionruntime.OriginCaller(),runtime.PreviousRealm(),runtime.CurrentRealm()- caller contextgno.land/p/nt/avl/v0- AVL tree (primary key-value storage)gno.land/p/nt/seqid/v0- monotonic ID generationgno.land/p/nt/ufmt/v0- string formattinggno.land/p/nt/urequire/v0/gno.land/p/nt/uassert/v0- test assertions
IBC voucher tokens (minted on RecvPacket for cross-chain tokens) use GRC20 tokens instead of native banker coins. This enables DeFi compatibility (Gnoswap, etc.) via the grc20reg registry.
gno.land/p/demo/tokens/grc20- GRC20 token implementation (NewToken,PrivateLedger.Mint/Burn)gno.land/r/demo/defi/grc20reg- Global token registry (Register,Get)
The ZKGM port is tracked in local_docs/zkgm/. Before changing ZKGM code, read the relevant wave plan/review there first. The main implementation paths are:
- ABI/types:
gno.land/p/core/ibc/zkgm/ - Proxy realm:
gno.land/r/core/ibc/apps/zkgm/ - v0 implementation:
gno.land/r/core/ibc/apps/zkgm/v0/impl/ - Mock receiver realm:
gno.land/r/core/ibc/apps/zkgm/testing/mock/
CallEnv.Calleris the tx origin / relayer identity captured by the ZKGM app request, not the deterministic proxy account.CallEnv.ProxyAccountcarriesPredictCallProxyAccount(path, destinationClient, sender).CallEnv.Relayercurrently mirrorsruntime.OriginCaller().String()as bytes.RelayerMsgis empty until the IBC core exposes relayer metadata.- Receiver realms must register with
zkgm.RegisterReceiver(cross, receiver)from their own realm. Tests should use the mock receiver realm instead of directly storing receiver instances from the impl package. - Mock receiver getters that read stored
[]bytefields must be crossing functions (cur realm) and should return strings/scalars, not structs containing slices.
The ZKGM implementation uses dispatcher helpers in v0/impl/dispatch.gno. Use these for new opcode integration:
dispatchVerifydispatchExecutedispatchAckdispatchTimeout
Batch children are intentionally limited to OP_CALL and OP_TOKEN_ORDER. Nested batch and forward children are rejected in v0. dispatchExecute must preserve types.RecvPacketResult.Status; do not reduce it to acknowledgement bytes only, or standalone Call failure status will be lost.
Batch acknowledgement rules:
- Child acknowledgements are collected into
BatchAck, then wrapped in outerAck{Tag: SUCCESS}. - A child
ACK_ERR_ONLY_MAKERis propagated as the parent batch acknowledgement and remaining children are skipped. - Universal-error batch ack is distributed to children as
types.UniversalErrorAcknowledgement()so TokenOrder children refund correctly. - Batch ack count must match child instruction count.
Forward v0 is limited by the current IBC core, which always writes an acknowledgement during RecvPacket. Full deferred parent acknowledgement propagation is not available yet.
- Forward children may be
OP_CALL,OP_TOKEN_ORDER, orOP_BATCH. Direct Forward-of-Forward input is rejected by verify, but multi-hop continuation rebuilds a nested Forward internally. executeForwardsends the child packet immediately and returns a success ack for the parent. Child ack/timeout later only looks up the parent ininFlightPacket, emits a ZKGM event, and clears the entry. Real parent ack writing needs a future IBC coreWriteAcknowledgement/ async-ack change.- Numeric path channels map through the temporary
channelToClient(uint32) -> "client-<id>"stub. Replace this with a registry before relying on real channel/client mappings. - Parent packet reconstruction currently sets
TimeoutTimestampto0becauseRecvRequestdoes not expose the original packet timeout. This is only suitable for lookup/event metadata until deferred acknowledgement handling exists. - The path helper can represent at most the uint256 channel slots it can build; do not assume a 9-hop overflow test can be constructed through
UpdateChannelPath.
- Treat
path *u256.Uintas read-only inside child handlers.gnoswap/uint256arithmetic mutates the receiver; copy first for derived paths:childPath := new(u256.Uint).Set(path) - Use
equalBytesforACK_ERR_ONLY_MAKERcomparisons. Avoidbytes.Equalon foreign or package-level byte slices because it may convert to string internally and panic on readonly tainted slices. - Use
cloneBytesbefore returning package-level[]bytevalues as acknowledgements.
The ZKGM impl currently relies mostly on focused unit tests, not filetests. For Batch and Call work, keep tests close to v0/impl/ and cover:
- direct handler behavior (
executeCall,executeBatch,acknowledgeBatch,timeoutBatch) - dispatcher routing from
Recv/Ack/Timeout - mixed opcode batches (
Call + TokenOrder) - only-maker ack propagation
- TokenOrder refund side effects via channel balance and voucher balance
- realm-boundary behavior through
testing/mock
For code that must produce byte-identical Solidity ABI output to Union (encoders/decoders for ZKGM packets, acks, instructions), use the fixture file at gno.land/p/core/encoding/abi/testdata/vectors.json. It is generated by the Rust harness at tools/abi-fixtures/ from Union's own sol! macro definitions. Tests should round-trip every applicable scenario (encode struct → assert hex equality, decode hex → assert field equality). Add new scenarios in the harness, then make refresh-abi-vectors and commit both copies.
ZKGM wire bytes follow the abi_encode_params flavor (no top-level head-offset prefix). Don't use plain abi.encode — it differs by 32 bytes at the start.
Files named z*_filetest.gno in realm directories. These are integration tests that run as standalone package main programs with expected output matching:
package main
import "gno.land/r/gnoswap/ibc/core"
func main() {
clientID := core.CreateClient(cross, clientState, consensusState)
println("CreateClient", clientID)
}
// Output:
// CreateClient 07-tendermint-1
// Events:
// [{"type": "create_client", "attrs": [...]}]
Naming convention: z{category}{letter}_{description}_filetest.gno
core realm: z1* = create client, z2* = update client, z3* = send packet, z5* = acknowledgement, z6* = timeout, z7* = recv packet, z8* = misbehaviour
transfer app: z0* = init, z1* = send packet, z2* = ack packet, z3* = timeout, z4* = recv packet, z5* = Transfer function. Double letters (e.g. z1aa) = IBC voucher token variant (vs z1a = native token)
zz_*_example_filetest.gno = documentation examples (referenced from README)
*_test.gno files use table-driven tests with a malleate function that mutates a default valid object to test specific conditions:
testCases := []struct {
name string
malleate func()
expErr string
}{
{"success", func() {}, ""},
{"failure: empty field", func() { msg.Field = "" }, "field required"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
msg = newValidMsg() // reset to valid defaults
tc.malleate() // apply mutation
err := msg.Validate()
// assert error
})
}
import (
"gno.land/p/nt/urequire" // fails test immediately
"gno.land/p/nt/uassert" // records failure, continues
)
urequire.NoError(t, err)
urequire.ErrorContains(t, err, "expected substring")
uassert.Equal(t, expected, actual)
- Realm — stateful smart contract (
.gno, underr/) - Package — stateless library (under
p/) - GnoVM — the interpreter
- gno.land — the blockchain
- Gnolang — the language
- ADR — architecture decision record
- ABCI — app-to-consensus interface
This file should stay concise, correct, and useful. Both humans and agents should help maintain it.
Humans: if you spot something wrong or missing, fix it directly — small PRs welcome. If an agent got confused by something, add a clarification here so the next one doesn't.
Agents: if you hit something misleading or missing in this doc during your work, flag it to the human and suggest a concrete edit. Include the fix in your PR when practical — future agents will benefit. If your human corrects you on something that this doc could have prevented, propose adding it.
The goal is a living document that gets sharper with each use.