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

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}