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}