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

store.gno

13.62 Kb · 441 lines
  1// Package store keeps Gnogolf's data (every record, best, board, standing,
  2// round in play and hole) apart from its rules, so a new version of the rules
  3// takes the data on instead of starting empty. It knows nothing of golf: a
  4// few ordered collections of string keys to string values, written only by
  5// the one realm it names as its writer (the rules: golf/v2 at the launch),
  6// read by anyone. A new writer takes over three days after its code is on
  7// chain and says so (the owner proposes it, it calls Ready), on its own; the
  8// owner can stop every write at once (Pause), start the same rules again at
  9// once (Resume), and renounce the role for good.
 10package store
 11
 12import (
 13	"chain"
 14	"chain/runtime/unsafe"
 15	"strconv"
 16	"strings"
 17	"time"
 18
 19	bptree "gno.land/p/nt/bptree/v0"
 20)
 21
 22// DELAY is how long a proposed writer waits, from its Ready, before it takes
 23// over: three days for anyone to read its code on gnoweb, which is on chain
 24// from then on and cannot change. A constant: it can never be shortened.
 25const DELAY = 72 * 3600 // seconds of block time
 26
 27// The bounds of every call: none can be made to run, or to store, unbounded.
 28const (
 29	maxKey   = 256
 30	maxValue = 64 << 10
 31	maxPage  = 300
 32	maxOps   = 64
 33	maxName  = 32
 34)
 35
 36var (
 37	self = unsafe.CurrentRealm().PkgPath()
 38	// writer is the rules' pkgpath, the only caller a write takes ("" paused);
 39	// proposed, the next one, taking over at at (block time, unix seconds; 0
 40	// until it calls Ready)
 41	writer   = strings.TrimSuffix(self, "store") + "golf/v2"
 42	proposed string
 43	at       int64
 44	// paused is the writer a Pause stopped, which Resume starts again ("" for none)
 45	paused string
 46	// owner may propose, cancel, pause and pass the role; "" once renounced
 47	owner   address
 48	pending address
 49	// the collections, by name: made at init, or by the writer (Make)
 50	colls = map[string]*bptree.BPTree{}
 51)
 52
 53// the collections v2 keeps its data in, made at deploy: a first save never
 54// pays a whole new tree
 55var standard = []string{"meta", "holes", "community", "data", "slots", "aliases", "authors", "hidden", "archived", "rounds", "bests", "boards", "totals", "ranks", "wear", "fresh", "copies"}
 56
 57func init() {
 58	owner = unsafe.OriginCaller()
 59	for _, n := range standard {
 60		colls[n] = seeded()
 61	}
 62}
 63
 64// seeded is a collection whose first leaf the deployer pays, not its first
 65// player: its key "", which no write can make (checkKey) and no read shows,
 66// sorts before every other.
 67func seeded() *bptree.BPTree {
 68	t := bptree.NewBPTree32()
 69	t.Set("", "")
 70	return t
 71}
 72
 73// --- who writes ----------------------------------------------------------
 74
 75// Writer is the realm whose writes store takes now: the proposed one once its
 76// delay is over ("" while paused).
 77func Writer() string {
 78	if matured() {
 79		return proposed
 80	}
 81	return writer
 82}
 83
 84func matured() bool { return proposed != "" && at != 0 && time.Now().Unix() >= at }
 85
 86// Proposed is the writer proposed and when it takes over (unix seconds; 0
 87// while its code has not called Ready), or "" and 0.
 88func Proposed() (string, int64) {
 89	if matured() {
 90		return "", 0
 91	}
 92	return proposed, at
 93}
 94
 95// settle makes a proposed writer whose delay is over the writer, as it is
 96// already for every read (Writer).
 97func settle() {
 98	if matured() {
 99		writer, proposed, at, paused = proposed, "", 0, ""
100		chain.Emit("WriterChanged", "writer", writer)
101	}
102}
103
104// Owner is the role that may propose, cancel and pause, "" once renounced.
105func Owner() address { return owner }
106
107// gate lets the writer through, and only it: the immediate caller's pkgpath,
108// never the signer (an account, a MsgRun script or another realm is refused).
109// A proposed writer whose delay is over becomes the writer at its first write.
110func gate(cur realm) {
111	w := Writer()
112	if w == "" {
113		panic("store: paused: no writes until the owner names new rules")
114	}
115	if cur.Previous().PkgPath() != w {
116		panic("store: only " + w + " writes here")
117	}
118	settle()
119}
120
121func onlyOwner(cur realm, what string) {
122	if owner == "" || cur.Previous().Address() != owner {
123		panic("store: only the owner can " + what)
124	}
125}
126
127// Propose names the next writer, a realm's pkgpath under gno.land/r/. Only the
128// owner can; a new proposal replaces the last. Its delay starts when that
129// realm, on chain, calls Ready: its code can then be read, as it will run.
130func Propose(cur realm, pkgpath string) {
131	onlyOwner(cur, "propose new rules")
132	if !validPkgPath(pkgpath) || pkgpath == self {
133		panic("store: the rules are a realm: gno.land/r/ and up to 100 of a-z, 0-9 and _-/.")
134	}
135	settle() // (one whose delay is over is the writer already)
136	proposed, at = pkgpath, 0
137	chain.Emit("WriterProposed", "writer", pkgpath)
138}
139
140// Ready is the proposed writer's own call, once it is on chain: it takes over
141// DELAY later, with no transaction to send. Only that realm can, once.
142func Ready(cur realm) {
143	if proposed == "" || at != 0 || cur.Previous().PkgPath() != proposed {
144		panic("store: only the proposed rules, once, say they are ready")
145	}
146	at = time.Now().Unix() + DELAY
147	chain.Emit("WriterReady", "writer", proposed, "at", strconv.FormatInt(at, 10))
148}
149
150// validPkgPath is golf v1's: gno.land/r/, up to 100 of a-z, 0-9 and _-/.,
151// no "", "." or ".." segment.
152func validPkgPath(p string) bool {
153	if len(p) > 100 || !strings.HasPrefix(p, "gno.land/r/") {
154		return false
155	}
156	for _, seg := range strings.Split(p[len("gno.land/r/"):], "/") {
157		if seg == "" || seg == "." || seg == ".." {
158			return false
159		}
160	}
161	for i := 0; i < len(p); i++ {
162		c := p[i]
163		if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || strings.IndexByte("_-/.", c) >= 0) {
164			return false
165		}
166	}
167	return true
168}
169
170// Cancel drops the proposed writer before it takes over. Only the owner can.
171func Cancel(cur realm) {
172	onlyOwner(cur, "cancel new rules")
173	if p, _ := Proposed(); p == "" {
174		panic("store: no new rules proposed")
175	}
176	drop()
177}
178
179// Pause stops every write at once (the reads go on), and drops any proposal.
180// Only the owner can: for a bug in the rules, or one feared. Resume starts the
181// same rules again at once; new rules take their delay.
182func Pause(cur realm) {
183	onlyOwner(cur, "pause")
184	settle() // (a takeover already in force is said, then stopped too)
185	drop()
186	if writer != "" {
187		paused = writer
188	}
189	writer = ""
190	chain.Emit("Paused")
191}
192
193// Resume starts again, at once, the rules a Pause stopped: their code was in
194// force already, so nothing new runs. Only the owner can.
195func Resume(cur realm) {
196	onlyOwner(cur, "resume")
197	settle()
198	if writer != "" || paused == "" {
199		panic("store: not paused")
200	}
201	writer, paused = paused, ""
202	chain.Emit("Resumed", "writer", writer)
203}
204
205// drop forgets a proposal not in force, and says so.
206func drop() {
207	if proposed != "" {
208		chain.Emit("WriterCancelled", "writer", proposed)
209	}
210	proposed, at = "", 0
211}
212
213// Transfer offers the owner's role, which the address offered takes with Accept.
214func Transfer(cur realm, to address) {
215	onlyOwner(cur, "pass the role")
216	if !to.IsValid() {
217		panic("store: not an address")
218	}
219	pending = to
220	chain.Emit("OwnerOffered", "to", to.String())
221}
222
223// Accept takes the role offered.
224func Accept(cur realm) {
225	if pending == "" || cur.Previous().Address() != pending {
226		panic("store: no transfer to you")
227	}
228	owner, pending = pending, ""
229	chain.Emit("OwnerChanged", "owner", owner.String())
230}
231
232// Renounce gives the role up for good: the writer can then never change, nor
233// be paused; a proposal not yet in force is dropped. Only the owner can, and
234// not while paused: no rules could ever write again.
235func Renounce(cur realm) {
236	onlyOwner(cur, "renounce")
237	settle()
238	if writer == "" {
239		panic("store: paused: name new rules before giving the role up, or nothing could ever be saved again")
240	}
241	drop()
242	owner, pending = "", ""
243	chain.Emit("OwnerRenounced")
244}
245
246// --- writes (the writer only) -----------------------------------------------
247
248func coll(name string) *bptree.BPTree {
249	t := colls[name]
250	if t == nil {
251		panic("store: no collection " + strconv.Quote(name))
252	}
253	return t
254}
255
256func checkKey(k string) {
257	if k == "" || len(k) > maxKey {
258		panic("store: a key is 1 to 256 bytes")
259	}
260}
261
262// (a string is a value: what store keeps is its own, never the caller's slice)
263func set(name, k, v string) {
264	checkKey(k)
265	if len(v) > maxValue {
266		panic("store: a value is at most 64 KiB")
267	}
268	coll(name).Set(k, v)
269}
270
271// Set keeps value under key in a collection. The writer only.
272func Set(cur realm, name, key, value string) {
273	gate(cur)
274	set(name, key, value)
275}
276
277// Remove drops a key from a collection, reporting whether it was there. The writer only.
278func Remove(cur realm, name, key string) bool {
279	gate(cur)
280	checkKey(key)
281	_, ok := coll(name).Remove(key)
282	return ok
283}
284
285// Batch applies a save's writes in one call: ops is "set", collection, key,
286// value or "del", collection, key, "" (four strings an op), at most 64 ops.
287// A transaction is all or nothing: a bad op undoes the whole batch.
288func Batch(cur realm, ops []string) {
289	gate(cur)
290	if len(ops)%4 != 0 || len(ops) > 4*maxOps {
291		panic("store: a batch is up to 64 ops of four strings")
292	}
293	for i := 0; i < len(ops); i += 4 {
294		switch ops[i] {
295		case "set":
296			set(ops[i+1], ops[i+2], ops[i+3])
297		case "del":
298			checkKey(ops[i+2])
299			coll(ops[i+1]).Remove(ops[i+2])
300		default:
301			panic("store: an op is set or del")
302		}
303	}
304}
305
306// Make adds a collection, for rules that need one more. The writer only.
307func Make(cur realm, name string) {
308	gate(cur)
309	if name == "" || len(name) > maxName || colls[name] != nil {
310		panic("store: a new collection's name is 1 to 32 bytes, not taken")
311	}
312	colls[name] = seeded()
313}
314
315// --- reads (anyone) ----------------------------------------------------------
316
317// Get is a key's value in a collection, and whether it is there.
318func Get(name, key string) (string, bool) {
319	if key == "" {
320		return "", false // (the seed)
321	}
322	v := coll(name).Get(key)
323	if v == nil {
324		return "", false
325	}
326	return v.(string), true
327}
328
329// Has reports whether a collection holds a key.
330func Has(name, key string) bool { return key != "" && coll(name).Has(key) }
331
332// Size is how many keys a collection holds.
333func Size(name string) int { return coll(name).Size() - 1 }
334
335// Index is how many keys of a collection sort before key: its place, from 0
336// (O(log n) a step of a binary search, as golf v1's boards).
337func Index(name, key string) int {
338	t := coll(name)
339	lo, hi := 0, t.Size()
340	for lo < hi {
341		mid := (lo + hi) / 2
342		if k, _ := t.GetByIndex(mid); k < key {
343			lo = mid + 1
344		} else {
345			hi = mid
346		}
347	}
348	if lo > 0 {
349		lo-- // (the seed, first: never counted)
350	}
351	return lo
352}
353
354// At is the key and value at a place in a collection's order.
355func At(name string, i int) (string, string) {
356	t := coll(name)
357	if i < 0 || i >= t.Size()-1 {
358		return "", ""
359	}
360	k, v := t.GetByIndex(i + 1)
361	return k, v.(string)
362}
363
364// Page is up to limit keys of a collection from start (included) to end
365// (excluded; "" to its end), in order, as rows one after another: each the
366// key's length, ":", the key, the value's length, ":", the value (no
367// separator to escape: the lengths say where each ends).
368// limit is 1 to 300.
369func Page(name, start, end string, limit int) string { return page(name, start, end, limit, false) }
370
371// PageBack is Page from end (kept, as bptree's back walk) down to start.
372func PageBack(name, start, end string, limit int) string { return page(name, start, end, limit, true) }
373
374// PageAt is up to limit keys from the offset-th of a collection (from 0), as Page's rows.
375func PageAt(name string, offset, limit int) string {
376	if limit < 1 || limit > maxPage {
377		panic("store: a page is 1 to 300 keys")
378	}
379	var sb strings.Builder
380	t := coll(name)
381	if offset < 0 {
382		offset = 0
383	}
384	if offset >= t.Size()-1 {
385		return "" // (and offset+1 cannot wrap)
386	}
387	t.IterateByOffset(offset+1, limit, func(k string, v any) bool { // (past the seed)
388		row(&sb, k, v.(string))
389		return false
390	})
391	return sb.String()
392}
393
394func page(name, start, end string, limit int, back bool) string {
395	if limit < 1 || limit > maxPage {
396		panic("store: a page is 1 to 300 keys")
397	}
398	var sb strings.Builder
399	n := 0
400	cb := func(k string, v any) bool {
401		if k == "" {
402			return false // (the seed)
403		}
404		row(&sb, k, v.(string))
405		n++
406		return n >= limit
407	}
408	if back {
409		coll(name).ReverseIterate(start, end, cb)
410	} else {
411		coll(name).Iterate(start, end, cb)
412	}
413	return sb.String()
414}
415
416// row writes one key and its value into a page.
417func row(sb *strings.Builder, k, v string) {
418	sb.WriteString(strconv.Itoa(len(k)) + ":" + k + strconv.Itoa(len(v)) + ":" + v)
419}
420
421// Render says who writes, what is proposed and who may change it.
422func Render(path string) string {
423	var sb strings.Builder
424	sb.WriteString("# Gnogolf's data\n\nEvery record, best, board and hole of the game, kept apart from its rules so that new rules take them on.\n\n")
425	if w := Writer(); w == "" {
426		sb.WriteString("**Paused:** no saves until the owner resumes the rules or names new ones. Everything can still be read.\n\n")
427	} else {
428		sb.WriteString("**The rules:** [" + w + "](" + strings.TrimPrefix(w, "gno.land") + ")\n\n")
429	}
430	if p, t := Proposed(); p != "" && t == 0 {
431		sb.WriteString("**New rules proposed:** " + p + ", not on chain yet: they take over three days after their code calls Ready, unless the owner cancels them.\n\n")
432	} else if p != "" {
433		sb.WriteString("**New rules proposed:** [" + p + "](" + strings.TrimPrefix(p, "gno.land") + "), taking over on " + time.Unix(t, 0).UTC().Format("2006-01-02 15:04 UTC") + " unless the owner cancels them. Read their code before then.\n\n")
434	}
435	if owner == "" {
436		sb.WriteString("**Nobody** can change the rules any more: the owner's role was given up.\n")
437	} else {
438		sb.WriteString("**The owner** (" + owner.String() + ") can propose new rules, which take over three days after their code is on chain, cancel them before, or stop every save at once and start the same rules again. Nothing else: no record can be written but by the rules.\n")
439	}
440	return sb.String()
441}