Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}