editions.gno
3.61 Kb · 123 lines
1package validator_scores
2
3import (
4 "strconv"
5 "strings"
6)
7
8// An edition keeps each candidate as its validated payload line, in the
9// order added. One string per candidate costs about 160 bytes of storage
10// deposit; a struct in an AVL tree cost about 3,700 bytes (measured on
11// gnodev at the gnoland-1 runtime).
12type edition struct {
13 number int
14 computedAt string
15 scorecardSHA256 string
16 arenas []string
17 sealed bool
18 lines []string // "operator|moniker|total|rank|github|s1,...,sN"
19}
20
21// editions holds the sealed editions in order, then at most one open
22// edition, always last.
23var editions []*edition
24
25// operatorLen is the length of a canonical g1 address, the prefix of a line.
26const operatorLen = 40
27
28// find returns the line stored for operator, if any.
29func (e *edition) find(operator string) (string, bool) {
30 if len(operator) != operatorLen {
31 return "", false
32 }
33 for _, line := range e.lines {
34 if line[:operatorLen] == operator {
35 return line, true
36 }
37 }
38 return "", false
39}
40
41func openEdition() *edition {
42 if len(editions) == 0 || editions[len(editions)-1].sealed {
43 return nil
44 }
45 return editions[len(editions)-1]
46}
47
48func requireOpen(number int) *edition {
49 e := openEdition()
50 if e == nil || e.number != number {
51 panic("validator_scores: edition " + strconv.Itoa(number) + " is not open")
52 }
53 return e
54}
55
56// OpenEdition starts a new edition and returns its number. Publisher only.
57// arenas is a comma-separated list, for example "challenge,test13,firedrill".
58func OpenEdition(cur realm, computedAt, scorecardSHA256, arenas string) int {
59 requirePublisher(cur)
60 if openEdition() != nil {
61 panic("validator_scores: an edition is already open")
62 }
63 if !isComputedAt(computedAt) {
64 panic("validator_scores: invalid computedAt")
65 }
66 if !isSHA256Hex(scorecardSHA256) {
67 panic("validator_scores: invalid scorecard SHA-256")
68 }
69 e := &edition{
70 number: len(editions) + 1,
71 computedAt: computedAt,
72 scorecardSHA256: scorecardSHA256,
73 arenas: parseArenas(arenas),
74 }
75 editions = append(editions, e)
76 return e.number
77}
78
79// AddCandidates adds one candidate per payload line to the open edition.
80// Publisher only. The whole payload is validated before anything is stored.
81func AddCandidates(cur realm, number int, payload string) {
82 requirePublisher(cur)
83 e := requireOpen(number)
84 if payload == "" {
85 panic("validator_scores: empty payload")
86 }
87 lines := strings.Split(payload, "\n")
88 seen := map[string]bool{}
89 for i, line := range lines {
90 operator := checkLine(line, len(e.arenas), i+1)
91 if _, found := e.find(operator); found || seen[operator] {
92 panic(lineError(i+1, "duplicate operator"))
93 }
94 seen[operator] = true
95 }
96 e.lines = append(e.lines, lines...)
97}
98
99// Seal closes the open edition for good. Publisher only. It fails unless the
100// edition holds exactly expectedCount candidates (at least one).
101func Seal(cur realm, number int, expectedCount int) {
102 requirePublisher(cur)
103 e := requireOpen(number)
104 if expectedCount < 1 {
105 panic("validator_scores: expectedCount must be at least 1")
106 }
107 if got := len(e.lines); got != expectedCount {
108 panic("validator_scores: edition holds " + strconv.Itoa(got) + " candidates, expected " + strconv.Itoa(expectedCount))
109 }
110 e.sealed = true
111}
112
113// DiscardOpen drops the open edition. Publisher only. Its number is reused
114// by the next OpenEdition. The slot is cleared first so the backing array
115// does not keep the discarded edition stored.
116func DiscardOpen(cur realm) {
117 requirePublisher(cur)
118 if openEdition() == nil {
119 panic("validator_scores: no open edition")
120 }
121 editions[len(editions)-1] = nil
122 editions = editions[:len(editions)-1]
123}