blocked.gno
4.62 Kb · 138 lines
1package nslogic
2
3import (
4 "strings"
5
6 "gno.land/r/nym-mnsrehearsal001/nsdata/v1"
7)
8
9/*
10THE BLOCK LIST: labels nobody may hold, at any price.
11
12This is NOT the reserved list, and confusing the two would undo both.
13
14 reserved (collections/reserved-labels.json, 232 labels) says
15 "blocked from the free tier, buyable at list price like anything
16 else". admin*meme being purchasable is deliberate: the harm it
17 guards against is handing a phishing address out for nothing, not
18 the existence of the name. That list is enforced by the issuer,
19 which is the only place that decides what is free.
20
21 THIS list says "not available, full stop". It is the small set where
22 the answer does not change when somebody offers money: names that
23 impersonate the registry itself, and the registry's own names.
24
25WHY IT LIVES IN THE VAULT'S GLOBALS RATHER THAN IN THIS FILE.
26
27Baking it in would mean a logic deploy to block a word, and the words
28worth blocking are exactly the ones discovered after launch — the label
29somebody tried yesterday that nobody thought of. Globals are writable by
30the admin, survive a logic swap because they belong to the vault, and
31cost storage only for what is actually stored.
32
33Chunked because a global value is capped at 512 bytes. Eight chunks is
34about four kilobytes, or several hundred labels, which is far more than
35this list should ever need — if it grows past that, the thing being
36built is a content policy and it belongs off chain where it can be
37updated without a transaction.
38
39FAILS OPEN, and that is the uncomfortable choice, so it is written down.
40An unreadable chunk blocks nothing rather than blocking everything.
41Failing closed here would mean one malformed global takes the whole
42registry offline — every registration, for everyone, until an admin
43notices. The harm from a blocked label slipping through is one bad name
44that an admin can freeze; the harm from failing closed is total. The
45list is also not the only defence: the issuer holds the reserved list,
46and FreezeName exists for anything that gets past both.
47*/
48
49const (
50 blockChunks = 8
51 blockKeyStem = "block"
52 blockChunkMax = 512
53)
54
55// blockKey is the global name for one chunk: block0 … block7.
56func blockKey(i int) string {
57 return blockKeyStem + string(rune('0'+i))
58}
59
60/*
61BlockedLabel reports whether a label may not be registered at all.
62
63Compares against the label as the registry stores it — lowercase, already
64validated — so a caller cannot slip past with different casing. Every
65comparison is exact: this list holds whole labels, not substrings,
66because "contains" matching turns a list of a dozen words into an
67unpredictable filter that refuses names nobody meant to refuse.
68*/
69func BlockedLabel(label string) bool {
70 if label == "" {
71 return false
72 }
73 want := strings.ToLower(label)
74 for i := 0; i < blockChunks; i++ {
75 v := nsdata.GetGlobal(blockKey(i))
76 if v == "" {
77 continue
78 }
79 for _, e := range strings.Split(v, ",") {
80 if strings.ToLower(strings.TrimSpace(e)) == want {
81 return true
82 }
83 }
84 }
85 return false
86}
87
88// BlockList returns one chunk, so the panel can show and edit what is
89// actually stored rather than what somebody believes is stored.
90func BlockList(i int) string {
91 if i < 0 || i >= blockChunks {
92 return ""
93 }
94 return nsdata.GetGlobal(blockKey(i))
95}
96
97// BlockChunks is the number of chunks, so the panel does not hard-code a
98// constant that this file might later change.
99func BlockChunks() int { return blockChunks }
100
101/*
102SetBlockList replaces one chunk. Admin only.
103
104Replaces rather than appends: an append-only list has no way to correct a
105mistake, and a mistake here refuses somebody a name for reasons nobody
106can see. The panel reads the chunk, edits it, and writes it back whole.
107*/
108func SetBlockList(cur realm, i int, csv string) {
109 assertNoSend(cur)
110 assertIsAdmin(cur)
111 emitAdmin(cur, "SetBlockList", "chunk", fmtI(i), "csv", csv)
112 if i < 0 || i >= blockChunks {
113 panic("nslogic: chunk out of range")
114 }
115 if len(csv) > blockChunkMax {
116 panic("nslogic: that chunk is longer than a global may be")
117 }
118 // Normalised on the way in, so the comparison above never has to
119 // account for whatever spacing somebody typed into a form.
120 parts := strings.Split(csv, ",")
121 out := []string{}
122 for _, p := range parts {
123 p = strings.ToLower(strings.TrimSpace(p))
124 if p != "" {
125 out = append(out, p)
126 }
127 }
128 setGlobal(cur, blockKey(i), strings.Join(out, ","))
129}
130
131// assertNotBlocked is the gate both doors pass through — the paid
132// registration and the free grant. One function, called twice, because
133// a block that only covers one route is not a block.
134func assertNotBlocked(label string) {
135 if BlockedLabel(label) {
136 panic("nslogic: " + label + " is not available")
137 }
138}