validate.gno
3.81 Kb · 146 lines
1package validator_scores
2
3import (
4 "strconv"
5 "strings"
6 "time"
7 "unicode/utf8"
8)
9
10const (
11 maxMonikerBytes = 64
12 maxArenas = 16
13 maxArenaName = 32
14 maxScore = 10000 // 100.00 in hundredths: total and each arena score
15 maxGithub = 1000 // 10.00 in hundredths: the GitHub bonus
16 maxRank = 100000
17)
18
19// isComputedAt accepts a UTC date ("2026-10-13") or a UTC RFC 3339 time
20// without fraction ("2026-10-13T09:00:00Z").
21func isComputedAt(s string) bool {
22 for _, layout := range []string{"2006-01-02", "2006-01-02T15:04:05Z"} {
23 if len(s) != len(layout) {
24 continue
25 }
26 t, err := time.Parse(layout, s)
27 if err == nil && t.Format(layout) == s {
28 return true
29 }
30 }
31 return false
32}
33
34func isSHA256Hex(s string) bool {
35 if len(s) != 64 {
36 return false
37 }
38 for i := 0; i < len(s); i++ {
39 c := s[i]
40 if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
41 return false
42 }
43 }
44 return true
45}
46
47// parseArenas splits a comma-separated list of 1 to 16 distinct names made
48// of [a-z0-9_-], each 1 to 32 bytes.
49func parseArenas(s string) []string {
50 names := strings.Split(s, ",")
51 if len(names) > maxArenas {
52 panic("validator_scores: too many arenas")
53 }
54 seen := map[string]bool{}
55 for _, name := range names {
56 if name == "" || len(name) > maxArenaName {
57 panic("validator_scores: invalid arena name")
58 }
59 for i := 0; i < len(name); i++ {
60 c := name[i]
61 if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_' || c == '-') {
62 panic("validator_scores: invalid arena name")
63 }
64 }
65 if seen[name] {
66 panic("validator_scores: duplicate arena " + name)
67 }
68 seen[name] = true
69 }
70 return names
71}
72
73// isMoniker rejects empty or oversized monikers, invalid UTF-8, ASCII and C1
74// control characters (U+0000-U+001F, U+007F, U+0080-U+009F), bidirectional
75// control characters (U+202A-U+202E, U+2066-U+2069), surrounding spaces, and
76// the separator and contact characters '|' and '@'. Zero-width and
77// directional marks (U+200B-U+200F) stay allowed: emoji monikers use U+200D.
78func isMoniker(s string) bool {
79 if s == "" || len(s) > maxMonikerBytes || !utf8.ValidString(s) || strings.TrimSpace(s) != s {
80 return false
81 }
82 for _, r := range s {
83 if r < 0x20 || (r >= 0x7f && r <= 0x9f) || r == '|' || r == '@' ||
84 (r >= 0x202a && r <= 0x202e) || (r >= 0x2066 && r <= 0x2069) {
85 return false
86 }
87 }
88 return true
89}
90
91// parseNat parses a canonical non-negative decimal (no sign, no leading
92// zero) not above max.
93func parseNat(s string, max int) (int, bool) {
94 if s == "" || len(s) > 7 || (len(s) > 1 && s[0] == '0') {
95 return 0, false
96 }
97 for i := 0; i < len(s); i++ {
98 if s[i] < '0' || s[i] > '9' {
99 return 0, false
100 }
101 }
102 n, err := strconv.Atoi(s)
103 if err != nil || n > max {
104 return 0, false
105 }
106 return n, true
107}
108
109func lineError(n int, msg string) string {
110 return "validator_scores: line " + strconv.Itoa(n) + ": " + msg
111}
112
113// checkLine validates "operator|moniker|total|rank|github|s1,...,sN" and
114// returns the operator. Every number must be canonical, so a stored line
115// can be copied into JSON as is.
116func checkLine(line string, arenaCount, n int) string {
117 f := strings.Split(line, "|")
118 if len(f) != 6 {
119 panic(lineError(n, "expected 6 fields"))
120 }
121 if !isCanonicalAddress(f[0]) {
122 panic(lineError(n, "invalid operator"))
123 }
124 if !isMoniker(f[1]) {
125 panic(lineError(n, "invalid moniker"))
126 }
127 if _, ok := parseNat(f[2], maxScore); !ok {
128 panic(lineError(n, "invalid total"))
129 }
130 if rank, ok := parseNat(f[3], maxRank); !ok || rank < 1 {
131 panic(lineError(n, "invalid rank"))
132 }
133 if _, ok := parseNat(f[4], maxGithub); !ok {
134 panic(lineError(n, "invalid github"))
135 }
136 scores := strings.Split(f[5], ",")
137 if len(scores) != arenaCount {
138 panic(lineError(n, "arena count mismatch"))
139 }
140 for _, v := range scores {
141 if _, ok := parseNat(v, maxScore); !ok {
142 panic(lineError(n, "invalid arena score"))
143 }
144 }
145 return f[0]
146}