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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

validate.gno

4.49 Kb · 117 lines
  1package nslogic
  2
  3import (
  4	"strings"
  5
  6	"gno.land/p/nt/ufmt/v0"
  7	"gno.land/r/nym-mnsrehearsal001/nsdata/v1"
  8)
  9
 10// THE LABEL RULE IS WRITTEN OUT, NOT COMPILED.
 11//
 12// It used to be a regexp, and that regexp was the single most expensive
 13// thing in this realm — by a wide margin, and not by a little. Measured
 14// on sapphire-1 with vm/qstorage against purpose-built probe realms:
 15//
 16//	a realm with code and no package-level state      2,711 bytes
 17//	the same realm plus ONE compiled regexp         154,877 bytes
 18//
 19// A compiled regexp is an object graph, and gno charges 100 ugnot for
 20// every byte of it, forever. That one variable was 15.2 GNOT of the
 21// 22.5 GNOT it costs to deploy this realm — about 68% of the bill, to
 22// enforce a rule that fits in thirty lines.
 23//
 24// The second regexp was worse, because it was compiled LAZILY and cached
 25// in a package-level var. That does not avoid the cost, it MOVES it: the
 26// first person to register a name after a fresh deploy would have had
 27// their transaction grow this realm's storage by 150KB and been charged
 28// ~15 GNOT of deposit for it. Confirmed by probe, not inferred — a realm
 29// deployed at 2,711 bytes went to 154,149 the moment one call compiled
 30// and cached a regexp, billed to the caller. Somebody buying a ten-cent
 31// name would have been asked for fifteen GNOT.
 32//
 33// The hand-written version is also 6x cheaper in gas on every call
 34// (1.06M against 6.67M), so there is no axis on which the regexp was
 35// the better choice. It read better. That was the whole of its case.
 36//
 37// WHAT THIS COSTS US. The pattern is no longer admin-tunable at runtime:
 38// changing the rule now means redeploying this realm. That is a fair
 39// trade at 7 GNOT a deploy, it was never a rule that should change
 40// casually, and SetLabelPattern refuses anything but the implemented
 41// pattern so the stored string can never describe rules the code does
 42// not enforce.
 43
 44// labelRule is the pattern this realm implements, kept as a string so
 45// nsdata, the error messages and the site all quote the same thing.
 46const labelRule = `^[a-z0-9]+(-[a-z0-9]+)*$`
 47
 48// matchesLabelRule is that pattern, by hand: lowercase letters and
 49// digits, single hyphens between them, none at either end.
 50func matchesLabelRule(s string) bool {
 51	if s == "" {
 52		return false
 53	}
 54	prevHyphen := true // a leading hyphen is not allowed
 55	for i := 0; i < len(s); i++ {
 56		c := s[i]
 57		switch {
 58		case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
 59			prevHyphen = false
 60		case c == '-':
 61			if prevHyphen {
 62				return false // leading, or two in a row
 63			}
 64			prevHyphen = true
 65		default:
 66			return false
 67		}
 68	}
 69	return !prevHyphen // nor a trailing one
 70}
 71
 72// looksLikeAddress was `^g1[a-z0-9]{20,38}$`. Anything shaped like a real
 73// gno.land address is refused as a label, so a name can never be mistaken
 74// for a raw wallet address in a UI.
 75func looksLikeAddress(s string) bool {
 76	if len(s) < 22 || len(s) > 40 || !strings.HasPrefix(s, "g1") {
 77		return false
 78	}
 79	for i := 2; i < len(s); i++ {
 80		c := s[i]
 81		if !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') {
 82			return false
 83		}
 84	}
 85	return true
 86}
 87
 88// validateLabel — same policy as the original single-realm build (see
 89// NOTES.md for the ENS/Starname/r/sys/users research behind it). The
 90// length bounds are still read from nsdata, so they survive a swap of
 91// this realm; the character rule is now this realm's own.
 92//
 93// Call sites, and why: RegisterName/RegisterDomain only (both the
 94// first-ever mint AND reclaiming a past-grace slot — a reclaim is a
 95// genuine new mint with a new owner). RenewName/RenewDomain deliberately
 96// do NOT call this — an already-registered name must keep renewing under
 97// whatever rule was in force when it was minted.
 98func validateLabel(label string) {
 99	minLen, maxLen := nsdata.GetLabelLength()
100	if len(label) < minLen || len(label) > maxLen {
101		panic(ufmt.Sprintf("nslogic: label must be %d-%d characters", minLen, maxLen))
102	}
103	// Structural invariant, enforced regardless of what the pattern
104	// happens to allow: "*" is the separator in both the token ID
105	// (label*domain) and nsdata's storage key (domain*label). A label or
106	// domain containing one would make those encodings ambiguous and let
107	// two different names collide onto a single record.
108	if strings.Contains(label, "*") {
109		panic("nslogic: label may not contain '*'")
110	}
111	if !matchesLabelRule(label) {
112		panic("nslogic: a name is lowercase letters, digits and single hyphens between them — " + labelRule)
113	}
114	if looksLikeAddress(label) {
115		panic("nslogic: label looks like a wallet address, not allowed")
116	}
117}