Transaction
E65F8FCB0529AA…C318C53E7331
Block 1,262,913 · index 0 · indexed
Summary
- Hash
- E65F8FCB0529AA3A2CFF4DBB971C79B5E2AC84699EE9205E5B28C318C53E7331
- Block
- 1,262,913
- Size
- 17289 bytes
- Gas used
- 27,249,686 / 80,000,000
- Fee
- 80000 ugnot
- Status
- success
Messages
- Attached funds
- 7000000 ugnot
Arguments · 17
- #1validator_scores
- #2admin.gno
- #3package validator_scores import "strings" // admin is the samcrew 2-of-3 multisig. Its only power is SetPublisher. const admin address = "g136j0m08pkm2lwwde9dmlx8uee26llent9s5cpf" var publisher address = initialPublisher // caller returns the frame-verified caller of a crossing function. func caller(cur realm) address { if !cur.IsCurrent() { panic("validator_scores: stale realm") } return cur.Previous().Address() } func requirePublisher(cur realm) { if caller(cur) != publisher { panic("validator_scores: publisher only") } } // isCanonicalAddress accepts only a lowercase, checksummed g1 address. func isCanonicalAddress(s string) bool { if len(s) != 40 || !strings.HasPrefix(s, "g1") || strings.ToLower(s) != s { return false } return address(s).IsValid() } // SetPublisher replaces the publisher. Admin only. func SetPublisher(cur realm, addr address) { if caller(cur) != admin { panic("validator_scores: admin only") } if !isCanonicalAddress(string(addr)) { panic("validator_scores: invalid publisher address") } publisher = addr } // GetAdmin returns the admin address, for post-deployment checks. func GetAdmin() string { return string(admin) } // GetPublisher returns the current publisher address. func GetPublisher() string { return string(publisher) }
- #4editions.gno
- #5package validator_scores import ( "strconv" "strings" ) // An edition keeps each candidate as its validated payload line, in the // order added. One string per candidate costs about 160 bytes of storage // deposit; a struct in an AVL tree cost about 3,700 bytes (measured on // gnodev at the gnoland-1 runtime). type edition struct { number int computedAt string scorecardSHA256 string arenas []string sealed bool lines []string // "operator|moniker|total|rank|github|s1,...,sN" } // editions holds the sealed editions in order, then at most one open // edition, always last. var editions []*edition // operatorLen is the length of a canonical g1 address, the prefix of a line. const operatorLen = 40 // find returns the line stored for operator, if any. func (e *edition) find(operator string) (string, bool) { if len(operator) != operatorLen { return "", false } for _, line := range e.lines { if line[:operatorLen] == operator { return line, true } } return "", false } func openEdition() *edition { if len(editions) == 0 || editions[len(editions)-1].sealed { return nil } return editions[len(editions)-1] } func requireOpen(number int) *edition { e := openEdition() if e == nil || e.number != number { panic("validator_scores: edition " + strconv.Itoa(number) + " is not open") } return e } // OpenEdition starts a new edition and returns its number. Publisher only. // arenas is a comma-separated list, for example "challenge,test13,firedrill". func OpenEdition(cur realm, computedAt, scorecardSHA256, arenas string) int { requirePublisher(cur) if openEdition() != nil { panic("validator_scores: an edition is already open") } if !isComputedAt(computedAt) { panic("validator_scores: invalid computedAt") } if !isSHA256Hex(scorecardSHA256) { panic("validator_scores: invalid scorecard SHA-256") } e := &edition{ number: len(editions) + 1, computedAt: computedAt, scorecardSHA256: scorecardSHA256, arenas: parseArenas(arenas), } editions = append(editions, e) return e.number } // AddCandidates adds one candidate per payload line to the open edition. // Publisher only. The whole payload is validated before anything is stored. func AddCandidates(cur realm, number int, payload string) { requirePublisher(cur) e := requireOpen(number) if payload == "" { panic("validator_scores: empty payload") } lines := strings.Split(payload, "\n") seen := map[string]bool{} for i, line := range lines { operator := checkLine(line, len(e.arenas), i+1) if _, found := e.find(operator); found || seen[operator] { panic(lineError(i+1, "duplicate operator")) } seen[operator] = true } e.lines = append(e.lines, lines...) } // Seal closes the open edition for good. Publisher only. It fails unless the // edition holds exactly expectedCount candidates (at least one). func Seal(cur realm, number int, expectedCount int) { requirePublisher(cur) e := requireOpen(number) if expectedCount < 1 { panic("validator_scores: expectedCount must be at least 1") } if got := len(e.lines); got != expectedCount { panic("validator_scores: edition holds " + strconv.Itoa(got) + " candidates, expected " + strconv.Itoa(expectedCount)) } e.sealed = true } // DiscardOpen drops the open edition. Publisher only. Its number is reused // by the next OpenEdition. The slot is cleared first so the backing array // does not keep the discarded edition stored. func DiscardOpen(cur realm) { requirePublisher(cur) if openEdition() == nil { panic("validator_scores: no open edition") } editions[len(editions)-1] = nil editions = editions[:len(editions)-1] }
- #6gnomod.toml
- #7module = "gno.land/r/samcrew/validator_scores" gno = "0.9"
- #8json.gno
- #9package validator_scores import ( "sort" "strconv" "strings" "unicode/utf8" ) const hexDigits = "0123456789abcdef" // quoteSafe quotes a string the realm validated to need no JSON escaping // (addresses, dates, hashes, arena names). Concatenation is cheap in gas; // per-byte loops are not. func quoteSafe(s string) string { return `"` + s + `"` } // quoteMoniker quotes a validated moniker (valid UTF-8, no control // characters): only '"' and '\' need escaping. func quoteMoniker(s string) string { if !strings.ContainsAny(s, `"\`) { return `"` + s + `"` } var b strings.Builder writeJSONString(&b, s) return b.String() } // writeJSONString writes any string as a JSON string. Invalid UTF-8 bytes // become U+FFFD. It costs gas per rune: use it only for short or unvalidated // input. func writeJSONString(b *strings.Builder, s string) { b.WriteByte('"') for i := 0; i < len(s); { r, size := utf8.DecodeRuneInString(s[i:]) switch { case r == utf8.RuneError && size == 1: b.WriteString(`\ufffd`) case r == '"': b.WriteString(`\"`) case r == '\\': b.WriteString(`\\`) case r < 0x20: b.WriteString(`\u00`) b.WriteByte(hexDigits[r>>4]) b.WriteByte(hexDigits[r&0xf]) default: b.WriteString(s[i : i+size]) } i += size } b.WriteByte('"') } func writeStrings(b *strings.Builder, list []string) { b.WriteByte('[') for i, s := range list { if i > 0 { b.WriteByte(',') } b.WriteString(quoteSafe(s)) } b.WriteByte(']') } // candidate is a stored line split into its fields. Numbers stay in their // canonical decimal text, which is valid JSON. type candidate struct { operator string moniker string total string rank int rankText string github string arenaScores string // "s1,...,sN" } func decode(line string) candidate { f := strings.Split(line, "|") rank, _ := strconv.Atoi(f[3]) return candidate{operator: f[0], moniker: f[1], total: f[2], rank: rank, rankText: f[3], github: f[4], arenaScores: f[5]} } type byRank []candidate func (s byRank) Len() int { return len(s) } func (s byRank) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s byRank) Less(i, j int) bool { if s[i].rank != s[j].rank { return s[i].rank < s[j].rank } if s[i].moniker != s[j].moniker { return s[i].moniker < s[j].moniker } return s[i].operator < s[j].operator } // sortedCandidates lists an edition's candidates by rank, then moniker // (byte order), then operator. func sortedCandidates(e *edition) []candidate { list := make([]candidate, len(e.lines)) for i, line := range e.lines { list[i] = decode(line) } sort.Sort(byRank(list)) return list } func latestSealed() *edition { for i := len(editions) - 1; i >= 0; i-- { if editions[i].sealed { return editions[i] } } return nil } // scoreFields is `"moniker":M,"total":T,"rank":R,"github":G`, shared by // both JSON shapes. func scoreFields(c candidate) string { return `"moniker":` + quoteMoniker(c.moniker) + `,"total":` + c.total + `,"rank":` + c.rankText + `,"github":` + c.github } func editionJSON(e *edition) string { var b strings.Builder b.WriteString(`{"number":` + strconv.Itoa(e.number) + `,"computedAt":` + quoteSafe(e.computedAt) + `,"scorecardSHA256":` + quoteSafe(e.scorecardSHA256) + `,"arenas":`) writeStrings(&b, e.arenas) b.WriteString(`,"candidates":[`) for i, c := range sortedCandidates(e) { if i > 0 { b.WriteByte(',') } b.WriteString(`{"operator":` + quoteSafe(c.operator) + `,` + scoreFields(c) + `,"arenaScores":[` + c.arenaScores + `]}`) } b.WriteString(`]}`) return b.String() } // LatestJSON returns the latest sealed edition, or "null". func LatestJSON() string { e := latestSealed() if e == nil { return "null" } return editionJSON(e) } // EditionJSON returns sealed edition n, or "null". func EditionJSON(n int) string { if n < 1 || n > len(editions) || !editions[n-1].sealed { return "null" } return editionJSON(editions[n-1]) } // CandidateJSON returns the operator's entries across sealed editions, in // edition order; "entries" is [] for an unknown address. func CandidateJSON(operator string) string { var b strings.Builder b.WriteString(`{"operator":`) writeJSONString(&b, operator) b.WriteString(`,"entries":[`) first := true for _, e := range editions { if !e.sealed { continue } line, ok := e.find(operator) if !ok { continue } if !first { b.WriteByte(',') } first = false c := decode(line) b.WriteString(`{"edition":` + strconv.Itoa(e.number) + `,"computedAt":` + quoteSafe(e.computedAt) + `,` + scoreFields(c) + `,"arenas":`) writeStrings(&b, e.arenas) b.WriteString(`,"arenaScores":[` + c.arenaScores + `]}`) } b.WriteString(`]}`) return b.String() } // OpenEditionJSON returns the open edition (number, scorecard hash and the // operators already added, in the order added), or "null". func OpenEditionJSON() string { e := openEdition() if e == nil { return "null" } var b strings.Builder b.WriteString(`{"number":` + strconv.Itoa(e.number) + `,"scorecardSHA256":` + quoteSafe(e.scorecardSHA256) + `,"operators":[`) for i, line := range e.lines { if i > 0 { b.WriteByte(',') } b.WriteString(quoteSafe(line[:operatorLen])) } b.WriteString(`]}`) return b.String() }
- #10publisher.gno
- #11package validator_scores // initialPublisher is the only writer of editions until the admin replaces it // with SetPublisher. Local gnodev runs stage a copy of the realm in which this // file names a gnodev test key instead. const initialPublisher address = "g136j0m08pkm2lwwde9dmlx8uee26llent9s5cpf"
- #12render.gno
- #13package validator_scores import ( "strconv" "strings" ) // formatHundredths renders the canonical decimal "8743" as "87.43". func formatHundredths(s string) string { for len(s) < 3 { s = "0" + s } return s[:len(s)-2] + "." + s[len(s)-2:] } // mdEscape backslash-escapes every ASCII punctuation character, so a // moniker renders as plain text and never as an author-chosen link, image // or HTML. gnoweb still turns a g1 address that follows a space into a link // to its /u/ page, with the address itself as the link text. func mdEscape(s string) string { var b strings.Builder for i := 0; i < len(s); i++ { c := s[i] if c >= '!' && c <= '/' || c >= ':' && c <= '@' || c >= '[' && c <= '`' || c >= '{' && c <= '~' { b.WriteByte('\\') } b.WriteByte(c) } return b.String() } // Render shows the latest sealed edition as a Markdown table. The path is // ignored. func Render(_ string) string { var b strings.Builder b.WriteString("# Validator selection scores\n\n") e := latestSealed() if e == nil { b.WriteString("No edition has been published yet.\n") return b.String() } b.WriteString("Edition " + strconv.Itoa(e.number) + ", computed at " + e.computedAt + ", scorecard SHA-256 `" + e.scorecardSHA256 + "`. Scores have two decimals; a sealed edition never changes.\n\n") b.WriteString("| Rank | Moniker | Operator | Total |") for _, a := range e.arenas { b.WriteString(" " + mdEscape(a) + " |") } b.WriteString(" GitHub |\n| ---: | --- | --- | ---: |") for range e.arenas { b.WriteString(" ---: |") } b.WriteString(" ---: |\n") for _, c := range sortedCandidates(e) { b.WriteString("| " + c.rankText + " | " + mdEscape(c.moniker) + " | " + c.operator + " | " + formatHundredths(c.total) + " |") for _, s := range strings.Split(c.arenaScores, ",") { b.WriteString(" " + formatHundredths(s) + " |") } b.WriteString(" " + formatHundredths(c.github) + " |\n") } b.WriteString("\nMachine-readable: `LatestJSON()`, `EditionJSON(n)`, `CandidateJSON(operator)`.\n") return b.String() }
- #14validate.gno
- #15package validator_scores import ( "strconv" "strings" "time" "unicode/utf8" ) const ( maxMonikerBytes = 64 maxArenas = 16 maxArenaName = 32 maxScore = 10000 // 100.00 in hundredths: total and each arena score maxGithub = 1000 // 10.00 in hundredths: the GitHub bonus maxRank = 100000 ) // isComputedAt accepts a UTC date ("2026-10-13") or a UTC RFC 3339 time // without fraction ("2026-10-13T09:00:00Z"). func isComputedAt(s string) bool { for _, layout := range []string{"2006-01-02", "2006-01-02T15:04:05Z"} { if len(s) != len(layout) { continue } t, err := time.Parse(layout, s) if err == nil && t.Format(layout) == s { return true } } return false } func isSHA256Hex(s string) bool { if len(s) != 64 { return false } for i := 0; i < len(s); i++ { c := s[i] if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') { return false } } return true } // parseArenas splits a comma-separated list of 1 to 16 distinct names made // of [a-z0-9_-], each 1 to 32 bytes. func parseArenas(s string) []string { names := strings.Split(s, ",") if len(names) > maxArenas { panic("validator_scores: too many arenas") } seen := map[string]bool{} for _, name := range names { if name == "" || len(name) > maxArenaName { panic("validator_scores: invalid arena name") } for i := 0; i < len(name); i++ { c := name[i] if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_' || c == '-') { panic("validator_scores: invalid arena name") } } if seen[name] { panic("validator_scores: duplicate arena " + name) } seen[name] = true } return names } // isMoniker rejects empty or oversized monikers, invalid UTF-8, ASCII and C1 // control characters (U+0000-U+001F, U+007F, U+0080-U+009F), bidirectional // control characters (U+202A-U+202E, U+2066-U+2069), surrounding spaces, and // the separator and contact characters '|' and '@'. Zero-width and // directional marks (U+200B-U+200F) stay allowed: emoji monikers use U+200D. func isMoniker(s string) bool { if s == "" || len(s) > maxMonikerBytes || !utf8.ValidString(s) || strings.TrimSpace(s) != s { return false } for _, r := range s { if r < 0x20 || (r >= 0x7f && r <= 0x9f) || r == '|' || r == '@' || (r >= 0x202a && r <= 0x202e) || (r >= 0x2066 && r <= 0x2069) { return false } } return true } // parseNat parses a canonical non-negative decimal (no sign, no leading // zero) not above max. func parseNat(s string, max int) (int, bool) { if s == "" || len(s) > 7 || (len(s) > 1 && s[0] == '0') { return 0, false } for i := 0; i < len(s); i++ { if s[i] < '0' || s[i] > '9' { return 0, false } } n, err := strconv.Atoi(s) if err != nil || n > max { return 0, false } return n, true } func lineError(n int, msg string) string { return "validator_scores: line " + strconv.Itoa(n) + ": " + msg } // checkLine validates "operator|moniker|total|rank|github|s1,...,sN" and // returns the operator. Every number must be canonical, so a stored line // can be copied into JSON as is. func checkLine(line string, arenaCount, n int) string { f := strings.Split(line, "|") if len(f) != 6 { panic(lineError(n, "expected 6 fields")) } if !isCanonicalAddress(f[0]) { panic(lineError(n, "invalid operator")) } if !isMoniker(f[1]) { panic(lineError(n, "invalid moniker")) } if _, ok := parseNat(f[2], maxScore); !ok { panic(lineError(n, "invalid total")) } if rank, ok := parseNat(f[3], maxRank); !ok || rank < 1 { panic(lineError(n, "invalid rank")) } if _, ok := parseNat(f[4], maxGithub); !ok { panic(lineError(n, "invalid github")) } scores := strings.Split(f[5], ",") if len(scores) != arenaCount { panic(lineError(n, "arena count mismatch")) } for _, v := range scores { if _, ok := parseNat(v, maxScore); !ok { panic(lineError(n, "invalid arena score")) } } return f[0] }
- #16/gno.MemPackageType
- #17 MPUserAll
Result log
msg:0,success:true,log:,events:[]