data.gno
16.51 Kb · 614 lines
1// Package data keeps GnoRadio's state apart from its rules. It knows nothing
2// of music: ordered collections of string keys to string values, and a vault
3// for the coins of promo budgets (vault.gno).
4//
5// Each collection belongs to a role, the prefix of its name ("catalog/…").
6// Only that role's writer realm writes it; anyone reads. A release (new
7// writers for one or more roles) takes over DELAY after every new realm said
8// Ready, all at once, and no data moves. The owner proposes and cancels
9// releases, pauses and resumes roles, passes the role on or renounces it, and
10// can replace the guardian 2x DELAY after saying so. The guardian can only
11// pause and cancel a release. This realm is never replaced: keep it small.
12package data
13
14import (
15 "chain"
16 "chain/runtime"
17 "chain/runtime/unsafe"
18 "strconv"
19 "strings"
20 "time"
21
22 "gno.land/p/nym-alexiscolin000/gnoradio/role/v0"
23 "gno.land/p/nym-alexiscolin000/gnoradio/store/v0"
24 bptree "gno.land/p/nt/bptree/v0"
25)
26
27// DELAY is how long a ready release waits before it takes over. It is a
28// constant so that it can never be shortened.
29const DELAY = 72 * 3600
30
31// Limits: every call is bounded.
32const (
33 maxKey = 128
34 maxValue = 16 << 10
35 maxOps = 64
36 maxPage = 100
37 maxColls = 32 // per role
38 maxCollName = 32
39 maxPath = 100
40)
41
42var roles = [4]string{"catalog", "radio", "tickets", "home"}
43
44var (
45 self string // this realm's path
46 owner *role.Role
47 guardian *role.Role
48 writer [4]string // pkgpath per role
49 paused [4]bool
50 proposed [4]string // "" = role unchanged by the release
51 ready [4]bool
52 at int64 // when the release takes over; 0 until every proposed realm said Ready
53 colls = map[string]*bptree.BPTree{}
54 nColls [4]int
55 past = bptree.NewBPTree32() // address -> pkgpath of every realm that has been a writer
56
57 frozen int64 // seconds some role has been paused, ended pauses only
58 frozeAt int64 // when the current pause of some role began, 0 if none
59
60 nextGuardian address // the owner's replacement for the guardian, "" if none
61 guardianAt int64 // when nextGuardian takes over
62)
63
64func init() {
65 self = unsafe.CurrentRealm().PkgPath()
66 deployer := unsafe.OriginCaller()
67 if deployer == "" && runtime.ChainID() == "dev" {
68 deployer = chain.PackageAddress(self + "/dev") // gno test: a key nobody holds
69 }
70 start(deployer)
71}
72
73// start puts release 1 in force (gno.land/r/<ns>/gnoradio/<role>/v1), with
74// the deployer as owner and guardian.
75func start(deployer address) {
76 owner = role.New("owner", deployer)
77 guardian = role.New("guardian", deployer)
78 base := strings.TrimSuffix(self, "data")
79 for i, r := range roles {
80 writer[i] = base + r + "/v1"
81 remember(writer[i])
82 }
83}
84
85// nftPath is the permanent ticket NFT realm (phase 5). It is not a writer:
86// it mints and transfers only for the tickets writer, and like every
87// GnoRadio realm it never takes vault payouts.
88func nftPath() string { return strings.TrimSuffix(self, "data") + "tickets/nft" }
89
90func remember(pkgpath string) { past.Set(chain.PackageAddress(pkgpath).String(), pkgpath) }
91
92func roleIndex(r string) int {
93 for i, x := range roles {
94 if x == r {
95 return i
96 }
97 }
98 panic("data: no role " + strconv.Quote(r))
99}
100
101// roleOf is the role of a collection or vault account: its name's prefix.
102func roleOf(name string) int {
103 r, _, _ := strings.Cut(name, "/")
104 return roleIndex(r)
105}
106
107func noPayment() {
108 if len(unsafe.OriginSend()) != 0 {
109 panic("data: this call takes no coins")
110 }
111}
112
113func now() int64 { return time.Now().Unix() }
114
115// ---- who writes ----
116
117func matured() bool { return at != 0 && now() >= at }
118
119// settle puts a release whose delay is over in force.
120func settle() {
121 if !matured() {
122 return
123 }
124 for i := range roles {
125 if proposed[i] != "" {
126 writer[i] = proposed[i]
127 remember(writer[i])
128 chain.Emit("WriterChanged", "role", roles[i], "writer", writer[i])
129 }
130 }
131 drop()
132}
133
134// drop forgets the release not yet in force.
135func drop() {
136 for i := range roles {
137 proposed[i], ready[i] = "", false
138 }
139 at = 0
140}
141
142// Writer is the realm that writes a role's collections now.
143func Writer(r string) string {
144 i := roleIndex(r)
145 if matured() && proposed[i] != "" {
146 return proposed[i]
147 }
148 return writer[i]
149}
150
151// gate lets only the writer of a collection's or account's role through,
152// and only while that role is not paused. The writer is the immediate
153// caller: accounts, MsgRun and the owner never pass.
154func gate(cur realm, name string) {
155 i := roleOf(name)
156 settle()
157 if paused[i] {
158 panic("data: " + roles[i] + " is paused")
159 }
160 if cur.Previous().PkgPath() != writer[i] {
161 panic("data: only " + writer[i] + " writes " + roles[i])
162 }
163}
164
165// IsGnoRadio reports whether addr is a GnoRadio realm: this one, the ticket
166// NFT realm, or any current, proposed or past writer.
167func IsGnoRadio(addr address) bool {
168 if role.Upper(addr) {
169 addr = address(strings.ToLower(addr.String())) // the upper-case spelling is the same account
170 }
171 if addr == chain.PackageAddress(self) || addr == chain.PackageAddress(nftPath()) || past.Has(addr.String()) {
172 return true
173 }
174 for i := range roles {
175 if proposed[i] != "" && addr == chain.PackageAddress(proposed[i]) {
176 return true
177 }
178 }
179 return false
180}
181
182// ---- the owner and the guardian ----
183
184func onlyOwner(cur realm) {
185 owner.Must(0, cur)
186 noPayment()
187 settle()
188}
189
190// checkPath refuses anything but a plain realm path.
191func checkPath(p string) {
192 if !strings.HasPrefix(p, "gno.land/r/") || len(p) > maxPath {
193 panic("data: a writer is a realm path of at most 100 characters")
194 }
195 for _, c := range p {
196 if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || strings.ContainsRune("_-/.", c)) {
197 panic("data: a realm path is a-z 0-9 _ - / .")
198 }
199 }
200 for _, s := range strings.Split(p, "/") {
201 if s == "" || s == "." || s == ".." {
202 panic("data: empty, . or .. in a realm path")
203 }
204 }
205}
206
207// Propose adds a role's new writer to the next release. Any change restarts
208// the clock. Owner only.
209func Propose(cur realm, r, pkgpath string) {
210 onlyOwner(cur)
211 i := roleIndex(r)
212 checkPath(pkgpath)
213 if pkgpath == self || pkgpath == nftPath() || pkgpath == writer[i] {
214 panic("data: a new writer is another realm")
215 }
216 proposed[i], ready[i], at = pkgpath, false, 0
217 chain.Emit("WriterProposed", "role", r, "writer", pkgpath)
218}
219
220// Ready is a proposed realm's own call (each rules realm exposes it): once
221// every proposed realm said it, the release takes over DELAY later.
222func Ready(cur realm) {
223 settle()
224 p := cur.Previous().PkgPath()
225 all, hit := true, false
226 for i := range roles {
227 if p != "" && proposed[i] == p && !ready[i] {
228 ready[i], hit = true, true
229 }
230 all = all && (proposed[i] == "" || ready[i])
231 }
232 if !hit {
233 panic("data: only a proposed realm says Ready, once")
234 }
235 if all {
236 at = now() + DELAY
237 chain.Emit("ReleaseReady", "at", strconv.FormatInt(at, 10))
238 }
239}
240
241// Cancel drops the release not yet in force. Owner or guardian. It never
242// touches a guardian replacement.
243func Cancel(cur realm) {
244 settleGuardian()
245 if !guardian.Is(0, cur) {
246 owner.Must(0, cur)
247 }
248 noPayment()
249 settle()
250 pending := false
251 for i := range roles {
252 pending = pending || proposed[i] != ""
253 }
254 if !pending {
255 panic("data: no release pending")
256 }
257 drop()
258 chain.Emit("ReleaseCancelled")
259}
260
261// Pause stops a role's writes and vault operations at once ("" every role);
262// reads and VaultWithdraw go on. on=false resumes the same writer. The owner
263// pauses and resumes; the guardian only pauses, and only while there is an
264// owner to resume.
265func Pause(cur realm, r string, on bool) {
266 settleGuardian()
267 if !(on && guardian.Is(0, cur) && owner.Holder() != "") {
268 owner.Must(0, cur)
269 }
270 noPayment()
271 settle()
272 before := anyPaused()
273 if r != "" {
274 paused[roleIndex(r)] = on
275 } else {
276 for i := range roles {
277 paused[i] = on
278 }
279 }
280 if after := anyPaused(); !before && after {
281 frozeAt = now()
282 } else if before && !after {
283 frozen, frozeAt = frozen+now()-frozeAt, 0
284 }
285 chain.Emit("Paused", "role", r, "on", strconv.FormatBool(on))
286}
287
288// Renounce gives the owner role up: writers never change again and nothing
289// can be paused. Refused while a role is paused, which could then never
290// write again. It drops the release not yet in force.
291func Renounce(cur realm) {
292 onlyOwner(cur)
293 for i := range roles {
294 if paused[i] {
295 panic("data: resume every role first")
296 }
297 }
298 drop()
299 owner.Renounce(0, cur)
300}
301
302// Offer offers the owner role to an address (offering it to the owner
303// cancels the offer); Accept takes it.
304func Offer(cur realm, to address) {
305 noPayment()
306 owner.Offer(0, cur, to)
307}
308
309func Accept(cur realm) {
310 noPayment()
311 owner.Accept(0, cur)
312}
313
314// GuardianOffer and GuardianAccept pass the guardian role in two steps; the
315// guardian offers it. The owner's only call on the role is GuardianReplace.
316func GuardianOffer(cur realm, to address) {
317 noPayment()
318 settleGuardian()
319 guardian.Offer(0, cur, to)
320}
321
322func GuardianAccept(cur realm) {
323 noPayment()
324 settleGuardian()
325 guardian.Accept(0, cur)
326}
327
328// GuardianRenounce gives the guardian role up. A replacement the owner
329// already announced still takes over.
330func GuardianRenounce(cur realm) {
331 noPayment()
332 settleGuardian()
333 guardian.Renounce(0, cur)
334}
335
336// GuardianReplace is the owner's way out of a lost or stolen guardian key,
337// which could otherwise cancel every release and pause after every resume
338// for good: to becomes the guardian 2x DELAY (six days) later. The guardian
339// cannot cancel it; the owner withdraws it with to = "" or announces another
340// one, which restarts the clock. The wait outlasts a release's DELAY, so a
341// stolen owner key that replaces the guardian is public for six days before
342// its own release can even start its 72 hours. Owner only.
343func GuardianReplace(cur realm, to address) {
344 onlyOwner(cur)
345 settleGuardian()
346 if to == "" {
347 nextGuardian, guardianAt = "", 0
348 chain.Emit("GuardianReplaceCancelled")
349 return
350 }
351 if !role.Canonical(to) {
352 panic("data: not an address")
353 }
354 nextGuardian, guardianAt = to, now()+2*DELAY
355 chain.Emit("GuardianReplacing", "to", to.String(), "at", strconv.FormatInt(guardianAt, 10))
356}
357
358// settleGuardian puts a replacement whose wait is over in force: a fresh
359// role, so any offer the old guardian left dies with it.
360func settleGuardian() {
361 if guardianAt == 0 || now() < guardianAt {
362 return
363 }
364 from := guardian.Holder()
365 guardian = role.New("guardian", nextGuardian)
366 nextGuardian, guardianAt = "", 0
367 chain.Emit("RoleChanged", "role", "guardian", "from", from.String(), "to", guardian.Holder().String())
368}
369
370// guardianNow is the guardian, a matured replacement included (reads do not
371// write it).
372func guardianNow() (holder, pending address) {
373 if guardianAt != 0 && now() >= guardianAt {
374 return nextGuardian, ""
375 }
376 return guardian.Holder(), guardian.Pending()
377}
378
379// Paused reports whether a role's writes are stopped now.
380func Paused(r string) bool { return paused[roleIndex(r)] }
381
382func anyPaused() bool {
383 for _, p := range paused {
384 if p {
385 return true
386 }
387 }
388 return false
389}
390
391func Owner() address { return owner.Holder() }
392
393func Guardian() address {
394 g, _ := guardianNow()
395 return g
396}
397
398// Writers is the state of every role, as JSON: {"owner","pendingOwner",
399// "guardian","pendingGuardian","nextGuardian","guardianAt","at","roles":
400// {"catalog":{"writer","paused","proposed","ready"},…}}. at is the unix time
401// a ready release takes over, guardianAt the time nextGuardian (the owner's
402// GuardianReplace) does.
403func Writers() string {
404 var sb strings.Builder
405 m, t := matured(), at
406 if m {
407 t = 0 // in force already
408 }
409 g, gp := guardianNow()
410 ng, ga := nextGuardian, guardianAt
411 if g == ng && ga != 0 && now() >= ga {
412 ng, ga = "", 0 // in force already
413 }
414 sb.WriteString(`{"owner":"` + owner.Holder().String() + `","pendingOwner":"` + owner.Pending().String() +
415 `","guardian":"` + g.String() + `","pendingGuardian":"` + gp.String() +
416 `","nextGuardian":"` + ng.String() + `","guardianAt":` + strconv.FormatInt(ga, 10) +
417 `,"at":` + strconv.FormatInt(t, 10) + `,"roles":{`)
418 for i, r := range roles {
419 w, p, rd := Writer(r), proposed[i], ready[i]
420 if m {
421 p, rd = "", false
422 }
423 if i > 0 {
424 sb.WriteString(",")
425 }
426 sb.WriteString(`"` + r + `":{"writer":"` + w + `","paused":` + strconv.FormatBool(paused[i]) +
427 `,"proposed":"` + p + `","ready":` + strconv.FormatBool(rd) + `}`)
428 }
429 sb.WriteString("}}")
430 return sb.String()
431}
432
433// ---- collections ----
434
435func coll(name string) *bptree.BPTree {
436 return colls[name]
437}
438
439func mustColl(name string) *bptree.BPTree {
440 t := coll(name)
441 if t == nil {
442 panic("data: no collection " + strconv.Quote(name))
443 }
444 return t
445}
446
447// Make creates a collection under the caller's role (nothing if it exists):
448// "<role>/<name>", name of a-z 0-9 _, at most 32 bytes in all, 32 per role.
449func Make(cur realm, name string) {
450 gate(cur, name)
451 if coll(name) != nil {
452 return
453 }
454 _, n, _ := strings.Cut(name, "/")
455 if n == "" || len(name) > maxCollName {
456 panic("data: a collection is <role>/<name>, at most 32 bytes")
457 }
458 for _, c := range n {
459 if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_') {
460 panic("data: a collection name is a-z 0-9 _")
461 }
462 }
463 i := roleOf(name)
464 if nColls[i] >= maxColls {
465 panic("data: 32 collections per role")
466 }
467 nColls[i]++
468 t := bptree.NewBPTree32()
469 t.Set("", "") // the first leaf is paid here, never by a first writer
470 colls[name] = t
471 chain.Emit("CollectionMade", "name", name)
472}
473
474// apply is one write of the caller's role: Set, Remove and each op of Batch.
475func apply(cur realm, op, name, k, v string) bool {
476 gate(cur, name)
477 if k == "" || len(k) > maxKey {
478 panic("data: a key is 1 to 128 bytes")
479 }
480 t := mustColl(name)
481 switch op {
482 case store.OpSet:
483 if len(v) > maxValue {
484 panic("data: a value is at most 16 KiB")
485 }
486 t.Set(k, v)
487 return true
488 case store.OpDel:
489 _, removed := t.Remove(k)
490 return removed
491 }
492 panic("data: an op is set or del")
493}
494
495// Set writes value at key of a collection of the caller's role.
496func Set(cur realm, name, key, value string) { apply(cur, store.OpSet, name, key, value) }
497
498// Remove deletes a key and reports whether it was there.
499func Remove(cur realm, name, key string) bool { return apply(cur, store.OpDel, name, key, "") }
500
501// Batch applies up to 64 ops of four strings (store.Ops): "set" or "del",
502// collection, key, value. It reads the strings and keeps nothing else of
503// the caller's slice.
504func Batch(cur realm, ops []string) {
505 if len(ops)%4 != 0 || len(ops) > 4*maxOps {
506 panic("data: a batch is up to 64 ops of four strings")
507 }
508 for i := 0; i < len(ops); i += 4 {
509 apply(cur, ops[i], ops[i+1], ops[i+2], ops[i+3])
510 }
511}
512
513// ---- reads ----
514
515// Get returns the value at key ("" and false when absent).
516func Get(name, key string) (string, bool) {
517 t := coll(name)
518 if t == nil || key == "" {
519 return "", false
520 }
521 v := t.Get(key)
522 if v == nil {
523 return "", false
524 }
525 return v.(string), true
526}
527
528func Has(name, key string) bool {
529 _, ok := Get(name, key)
530 return ok
531}
532
533// Size is the number of keys of a collection.
534func Size(name string) int {
535 if t := coll(name); t != nil {
536 return t.Size() - 1
537 }
538 return 0
539}
540
541// Index is the number of keys before key: Index(c, b) - Index(c, a) counts
542// the keys in [a, b) without a counter. O(log² n).
543func Index(name, key string) int {
544 t := coll(name)
545 if t == nil || key == "" {
546 return 0
547 }
548 lo, hi := 1, t.Size() // index 0 is the seed ""
549 for lo < hi {
550 mid := (lo + hi) / 2
551 if k, _ := t.GetByIndex(mid); k < key {
552 lo = mid + 1
553 } else {
554 hi = mid
555 }
556 }
557 return lo - 1
558}
559
560// Page returns up to limit (1-100) rows with start <= key < end ("" no
561// bound), lowest first or highest first when reverse, as store.Row strings
562// (store.Rows decodes them).
563func Page(name, start, end string, limit int, reverse bool) string {
564 checkLimit(limit)
565 t := coll(name)
566 if t == nil {
567 return ""
568 }
569 var sb strings.Builder
570 n := 0
571 cb := func(k string, v any) bool {
572 if k == "" || reverse && k == end {
573 return false // the seed, or end in reverse (ReverseIterate keeps it)
574 }
575 sb.WriteString(store.Row(k, v.(string)))
576 n++
577 return n >= limit
578 }
579 if reverse {
580 t.ReverseIterate(start, end, cb)
581 } else {
582 t.Iterate(start, end, cb)
583 }
584 return sb.String()
585}
586
587// PageAt returns up to limit (1-100) rows from the offset-th key, counted
588// from the highest when reverse.
589func PageAt(name string, offset, limit int, reverse bool) string {
590 checkLimit(limit)
591 t := coll(name)
592 if t == nil || offset < 0 || offset >= t.Size() {
593 return ""
594 }
595 var sb strings.Builder
596 cb := func(k string, v any) bool {
597 if k != "" { // the seed comes last in reverse
598 sb.WriteString(store.Row(k, v.(string)))
599 }
600 return false
601 }
602 if reverse {
603 t.ReverseIterateByOffset(offset, limit, cb)
604 } else {
605 t.IterateByOffset(offset+1, limit, cb)
606 }
607 return sb.String()
608}
609
610func checkLimit(limit int) {
611 if limit < 1 || limit > maxPage {
612 panic("data: a page is 1 to 100 rows")
613 }
614}