const ModeClosed, ModeOwner, ModeOpen, ModeAllowlist, ModePaid
Registration-policy modes.
Package gns implements GNS (Gno Name Service): an ENS-equivalent naming system expressed as a single Gno-native realm.
GNS is an ENS-equivalent naming system expressed as a single Gno-native realm. It provides the capabilities mainstream ENS users rely on — registration, renewal, expiry & grace, forward/reverse resolution, primary names, typed and arbitrary records, subnames with policies, delegated operators, pagination and events — but implements all of it through one realm, one ownership model, one record model, one authorization function and one registration lifecycle, rather than a collection of emulated ENS contracts.
Package path: gno.land/r/moul/gns/v1 · possible future target: gno.land/r/gnoland/gns.
Built and tested against gno 0.9 / master (chain, chain/banker,
chain/runtime, gno.land/p/nt/avl/v0).
GNS deliberately does not aim for byte-for-byte ENS/Ethereum compatibility.
Equivalent user capabilities (different implementation):
| ENS capability | GNS |
|---|---|
| Register / renew second-level name | Register, Renew |
| Expiration + grace period | stored expiry + configurable grace |
| Commit–reveal | Commit + Register (see preimage below) |
| Owner / transfer | OwnerOf, Transfer |
| Resolver records | built-in typed records (no pluggable resolver) |
| Multichain addresses | SetCoinAddress keyed by coin type |
| Text / content hash / pubkey / ABI / interface | typed setters/getters |
| Arbitrary records | SetRecord with reverse-DNS namespaces |
| Reverse resolution + primary name | SetPrimaryName / PrimaryName (forward-verified) |
| Subnames + subname registrar | hierarchical names + RegistrationPolicy |
| Wrapped / emancipated names | explicit ControlPolicy flags + LockPolicy |
| Delegated managers | SetOperator with per-permission grants |
| Multicall | not in-realm — loop a typed setter from one gnokey maketx run |
| Wildcard resolution | Resolve(..., NearestAncestor) |
| Events | append-only Event log + native chain.Emit |
Explicitly NOT included (Ethereum-specific or out of scope for v1): DNSSEC import, DNS registrar, CCIP-Read, L2 resolution, NFT/ERC-721 ownership, Ethereum ABI compatibility, Unicode/emoji names, governance DAO, auctions, secondary marketplace, and arbitrary custom-resolver execution.
alice,
not alice.gno. Display clients may append a .gno suffix; it is presentation
only.label.parent (wallet.alice, prod.api.company).a–z, digits 0–9, and - (not leading/trailing).
1–63 bytes per label, ≤255 bytes and ≤16 labels total. Input is lowercased;
non-ASCII is rejected. All canonicalization happens in Normalize.Available → Committed → Active → Grace → Expired → (recycled), plus Deleted
for removed subnames and Reserved for admin-held names.
now < ExpiresAt. Owner and permitted operators may mutate.ExpiresAt ≤ now < GraceEndsAt. Only renewal by the existing owner;
not registrable by others.now ≥ GraceEndsAt. Registrable again; old state is cleared on
re-registration and Generation is incremented so clients can detect the
replacement. Subnames carry ParentGeneration and do not silently
survive a recycled parent.The commitment binds the reveal so observers can neither copy nor front-run it:
commitment = sha256hex( name | owner | duration | secret | recordsHash | policyRevision )
where | is "|", owner is the bech32 string, integers are base-10, and
name is the normalized name. Use the on-chain helper MakeCommitment(...)
to compute it identically to what Register recomputes at reveal.
Deterministic and boring — no oracle, no USD, no auction:
price = duration × BasePricePerSecond × lengthMultiplier(label)
Default length multipliers: 1→100, 2→25, 3→5, 4→2, 5+→1. Price(name, duration)
returns a quote; Register always recomputes from state and rejects a stale
policyRevision.
Every mutation funnels through one internal authorize(caller, name, permission)
with a fixed authority order:
ControlPolicy allows it.The spec is written with Ethereum/Go idioms; these are the deliberate, gno-correct adaptations:
error. In gno only a panic/abort
reverts state, so state-changing crossing functions (those taking cur realm)
panic with a stable machine-readable error code (unauthorized,
name_unavailable, commitment_missing, …). Read/quote functions return
(value, ok) or (value, error) normally.avl.Tree instead of Go maps for every enumerable collection, so all
listing APIs (NamesByOwner, Subnames, TextKeys, CoinTypes,
Operators, EventsAfter) are ordered, bounded, and cursor-based — there is
no unbounded "return everything" query.Read: Normalize, Status, Exists, OwnerOf, GetName, Resolve,
Address, CoinAddress, Text, ContentHash, PublicKey, ABI,
Interface, Record, PrimaryName, Price, MakeCommitment,
CommitmentStatus, NamesByOwner, Subnames, TextKeys, CoinTypes,
Operators, EventsAfter, EventsForName, Render.
Mutations (crossing): Commit, Register, Renew, Transfer,
CreateSubname, DeleteSubname, SetRegistrationPolicy, LockPolicy,
SetOperator, RemoveOperator, SetPrimaryName,
ClearPrimaryName, and the typed record setters (SetAddress, SetText,
SetCoinAddress, SetContentHash, SetPublicKey, SetABI, SetInterface,
SetRecord, SetTTL).
Admin (two-step transfer): SetPaused, SetRegistrationOpen, SetPricing,
SetTreasury, ReserveName, SetLimits, TransferAdmin, AcceptAdmin.
Render(path) serves a read-only Markdown explorer:
/ overview + stats
/name/<name> owner, status, expiry, records, subnames
/address/<g1...> verified primary name + owned names
/available/<name> availability + price
/events recent events
/help API summary
1export GNOROOT=/path/to/gnolang/gno # a gno master checkout
2gno lint .
3gno test .
The test suite covers the spec's critical invariants: single effective owner, expired owners lose authority, grace names aren't re-registrable, parents can't exceed child policy, permanent policies only tighten, forward-verified primary names, single-use commitments, deterministic overflow-safe pricing, generation recycling without stale-record leakage, bounded enumeration, and admin non-confiscation.
Testing note. Because this realm is developed outside the gno examples module, it uses local assertion helpers instead of
gno.land/p/nt/uassert(whose working-tree copy fails to preprocess for external packages), and it unit-tests rejection paths against the internal error-returning helpers (authorize,priceFor,available,mergeRestrictive, …) rather than by catching crossing-boundary aborts. End-to-end abort/// Error:filetests can be added once the realm lives in-tree.
Reverse resolution is forward-verified on-chain, so clients and indexers may safely cache the user ↔ address mapping:
This is a v1 implementation of the GNS design spec. It is staged for review; it is not deployed.
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:

⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.
Package gns implements GNS (Gno Name Service): an ENS-equivalent naming system expressed as a single Gno-native realm.
The design goal is NOT byte-for-byte ENS compatibility. Instead it provides the same user-facing capabilities (registration, renewal, expiry/grace, forward & reverse resolution, primary names, typed + arbitrary records, subnames with policies, delegated operators, pagination and events) through ONE realm, ONE ownership model, ONE record model, ONE authorization function and ONE registration lifecycle.
Key deviations from a naive port of the spec, forced by gno semantics:
See README.md for the full compatibility statement.
Registration-policy modes.
1const (
2 EvNameRegistered = "NameRegistered"
3 EvNameRenewed = "NameRenewed"
4 EvNameTransferred = "NameTransferred"
5 EvNameExpired = "NameExpired"
6 EvNameDeleted = "NameDeleted"
7 EvSubnameCreated = "SubnameCreated"
8 EvPolicyChanged = "PolicyChanged"
9 EvOperatorChanged = "OperatorChanged"
10 EvRecordChanged = "RecordChanged"
11 EvPrimaryNameChange = "PrimaryNameChanged"
12 EvConfigChanged = "ConfigChanged"
13 EvPaused = "Paused"
14 EvUnpaused = "Unpaused"
15)Event types.
ABI returns an ABI blob.
AcceptAdmin completes the two-step admin handover.
Address returns the native address record.
ClearPrimaryName clears the caller's reverse record.
CoinAddress returns a multichain address.
Commit stores a registration commitment. The commitment hides the intended name; only its hash is recorded together with the committer and timestamp.
ContentHash returns the content hash record.
1func CreateSubname(cur realm, parent string, label string, owner address, options SubnameOptions)CreateSubname creates label.parent according to the parent registration policy.
DeleteSubname removes a subname. Callable by the owner, or by the parent when ParentCanDelete is set.
Exists reports whether a name currently resolves to a live registration.
Interface returns an interface target.
LockPolicy makes a name's control policy strictly more restrictive (emancipation). Flags may only move true->false; once Permanent, no field may be relaxed. Only the owner may lock.
1func MakeCommitment(name string, owner address, duration int64, secret, recordsHash string, policyRevision uint64) (string, error)MakeCommitment is the public helper clients use to derive the commitment hex to pass to Commit. It normalizes the name first so the value matches what Register recomputes at reveal. Returns an error if the name is invalid.
Normalize canonicalizes a name: lowercases ASCII, validates every label, and enforces length/depth limits. Non-ASCII input is rejected outright.
OwnerOf returns the owner of an active/grace name.
PrimaryName returns the verified primary name for an address, checking that (1) a reverse record exists, (2) the name is active, and (3) forward resolution still matches. Any failure returns not-found.
PublicKey returns the public key record.
Record returns an arbitrary namespaced record.
RemoveOperator revokes an operator.
Render is a human-readable explorer. It never mutates state.
ReserveName reserves (or unreserves) an unregistered name so it cannot be publicly registered. Admin may not reserve an actively-owned name.
SetABI sets an ABI blob by content type.
SetAddress sets the native address record.
SetCoinAddress sets a multichain address for coinType (decimal string).
SetContentHash sets the content hash record.
SetInterface sets an interface target by interface ID.
SetLimits updates operational storage/abuse limits (future registrations and mutations). Existing names are unaffected until next mutation.
SetOperator grants (or updates) an operator's permissions on a name.
SetPaused toggles the global pause. Paused blocks registration, subname creation, transfers and record mutation; reads, renewals and primary-name clearing remain available. It never confiscates or mutates ownership.
SetPricing updates future pricing and bumps the policy revision so pending commitments that priced against the old rules are rejected at reveal.
SetPrimaryName sets the caller's primary (reverse) name. The name must be active and forward-resolve (Address) to the caller.
SetPublicKey sets the public key record.
SetRecord sets an arbitrary namespaced record. Reserved namespaces are rejected; use the typed setters for those.
SetRegistrationOpen toggles whether new second-level registrations are open.
SetRegistrationPolicy sets the subname-issuance policy for a name.
SetTTL sets the name's TTL metadata.
SetText sets a text record; empty value deletes it (physical removal).
SetTreasury updates the treasury address.
Text returns a text record.
Transfer moves ownership of a name.
TransferAdmin begins a two-step admin handover.
CommitmentStatus returns the state of a pending commitment.
EventsAfter returns events with ID strictly greater than id.
EventsForName returns events for a specific name with ID greater than after.
Status returns the lifecycle status of a name.
GetName returns a read-only view of a name.
Operators lists operators and their permissions for a name.
Price returns a price quote for registering/renewing name for duration.
Register reveals and consumes a commitment to create a second-level name.
Renew extends a name's expiry. Anyone may pay to renew (a socially useful property: third parties can prevent expiry but gain no authority). Renewal is allowed while Active or in Grace, never once fully Expired.
Resolve returns a record either at the exact name or from the nearest valid ancestor (wildcard-style). Inheritance is explicit, never implicit in the primitive getters.
CoinTypes lists multichain coin types set for a name.
NamesByOwner lists canonical names owned by owner.
Subnames lists direct subnames of parent.
TextKeys lists text-record keys for a name.
CommitmentView is the read projection of a pending commitment.
1type Config struct {
2 Admin address
3 PendingAdmin address
4
5 RegistrationOpen bool
6
7 MinCommitAge int64
8 MaxCommitAge int64
9
10 MinRegistrationDuration int64
11 MaxRegistrationDuration int64
12
13 GracePeriod int64
14
15 BasePricePerSecond int64
16 PremiumByLength map[uint8]int64
17
18 PaymentDenom string
19 Treasury address
20
21 MaxTextValueBytes uint32
22 MaxBinaryValueBytes uint32
23 MaxRecordsPerName uint16
24 MaxOperatorsPerName uint16
25
26 PolicyRevision uint64 // bumped whenever pricing/registration rules change
27 Paused bool
28}Config is the realm-global configuration.
1type ControlPolicy struct {
2 OwnerCanTransfer bool
3 OwnerCanCreateSubnames bool
4 RecordsMutable bool
5
6 ParentCanReclaim bool
7 ParentCanTransfer bool
8 ParentCanDelete bool
9 ParentCanChangePolicy bool
10
11 // Permanent means the policy can only become MORE restrictive. It does not
12 // make records immutable by itself.
13 Permanent bool
14}ControlPolicy holds the explicit, readable ownership/parent control flags that replace ENS Name Wrapper fuses.
Event is an append-only change record for indexers.
1type Name struct {
2 Canonical string
3 Owner address
4
5 CreatedAt int64
6 UpdatedAt int64
7 ExpiresAt int64 // 0 == permanent subname (follows parent validity)
8 GraceEndsAt int64
9
10 Parent string
11 Label string
12 Depth uint8
13
14 TTL uint64
15
16 Generation uint64
17 ParentGeneration uint64
18
19 RegistrationPolicy RegistrationPolicy
20 ControlPolicy ControlPolicy
21
22 Records *Records
23
24 Operators *avl.Tree // address string -> Permissions
25 OperatorCount int
26
27 Revision uint64
28 Reserved bool
29 Deleted bool
30}Name is the single object the whole realm operates on.
NameStatus mirrors the lifecycle states.
NameView is the read-only projection returned by GetName.
Permission enumerates the delegable operator capabilities.
Permissions is an operator grant. ExpiresAt == 0 means no expiry.
PriceQuote is returned by Price.
PricingConfig is the admin-settable pricing surface.
RecordQuery selects which record Resolve should return.
1type Records struct {
2 NativeAddress string
3 ContentHash []byte
4 PublicKey []byte
5
6 Addresses *avl.Tree // coinType (decimal string) -> []byte
7 Text *avl.Tree // key -> string
8 ABIs *avl.Tree // contentType -> []byte
9 Interfaces *avl.Tree // interfaceID -> string
10 Arbitrary *avl.Tree // "namespace/key" -> []byte
11
12 Count int // number of stored entries, for MaxRecordsPerName enforcement
13}Records is the built-in resolver state for a single name.
RegisterRequest is the reveal payload for Register.
The commitment the client submits via Commit MUST equal sha256hex(Name|Owner|Duration|Secret|RecordsHash|PolicyRevision) using the same field values. RecordsHash is an opaque client-computed hex digest of the intended initial records; it binds the reveal so a front-runner cannot change records. NativeAddress/SetPrimary are optional conveniences applied after creation.
RegistrationPolicy governs how subnames of a name may be created. The realm-global config drives second-level registration.
RegistrationResult is returned by Register.
RenewalResult is returned by Renew.
ResolveMode selects exact vs inherited resolution.
ResolveResult is returned by Resolve.
Paged result types (gno avoids generics; concrete types keep it simple).
SubnameOptions configures CreateSubname.