export.gno
5.23 Kb · 142 lines
1package nsdata
2
3import (
4 "strings"
5
6 "gno.land/p/nt/avl/v0"
7)
8
9// Snapshot support: the other half of the contingency plan.
10//
11// Governance protects against a stolen or lost key. This file protects
12// against the case no key can fix — a VM-level corruption, a chain
13// migration, a defect in this realm discovered too late — where the only
14// way out is to reconstruct the registry somewhere else. That is
15// possible only if the whole state can be read out reliably and
16// repeatably, which needs pagination: an unpaginated listing of a
17// registry with tens of thousands of names is one query that is too
18// large to ever return, and a snapshot you cannot take is not a backup.
19//
20// Recovery shape, should it ever be needed:
21// 1. Pause() — freeze the registry so it cannot drift from the snapshot.
22// 2. Page through the exports here to reconstruct state off-chain.
23// 3. Deploy a successor, import the snapshot.
24// 4. SetSuccessor() on this realm so readers can follow the move.
25//
26// The exports are also just useful: an operator who takes a snapshot
27// every week has an off-chain record independent of any single chain.
28
29const maxPage = 200
30
31// pageKeys walks a key span and returns up to `limit` keys after the
32// `after` cursor, plus the cursor to pass next (empty when exhausted).
33func pageKeys(t *avl.Tree, start, end, after string, limit int) (csv, next string) {
34 if limit <= 0 || limit > maxPage {
35 limit = maxPage
36 }
37 from := start
38 if after != "" && after > from {
39 from = after
40 }
41 var b strings.Builder
42 n := 0
43 hasMore := false
44 last := ""
45 t.Iterate(from, end, func(key string, v any) bool {
46 if after != "" && key <= after {
47 return false
48 }
49 if n >= limit {
50 hasMore = true
51 return true
52 }
53 if n > 0 {
54 b.WriteString(",")
55 }
56 b.WriteString(key)
57 last = key
58 n++
59 return false
60 })
61 if !hasMore {
62 last = ""
63 }
64 return b.String(), last
65}
66
67// ExportDomains pages through every domain label.
68func ExportDomains(after string, limit int) (labels, next string) {
69 return pageKeys(&domains, "", "", after, limit)
70}
71
72// ExportNames pages through every name in the registry, as raw storage
73// keys (`domain*label`). Pass an empty domainLabel to walk the whole
74// tree, or a domain to walk just that domain's span.
75func ExportNames(domainLabel, after string, limit int) (keys, next string) {
76 if domainLabel == "" {
77 return pageKeys(&names, "", "", after, limit)
78 }
79 start, end := domainRange(domainLabel)
80 return pageKeys(&names, start, end, after, limit)
81}
82
83// ExportPrimary pages through the reverse-resolution table as owner
84// addresses; pair with GetPrimary to read each value.
85func ExportPrimary(after string, limit int) (owners, next string) {
86 return pageKeys(&primary, "", "", after, limit)
87}
88
89// ExportGlobalKeys lists the package-level extension slot's keys; pair
90// with GetGlobal for values.
91func ExportGlobalKeys(after string, limit int) (keys, next string) {
92 return pageKeys(&globals, "", "", after, limit)
93}
94
95// ExportAccounts pages through the per-address store as raw keys
96// `<address>:<field>`; pair with GetAccount for each value. Field names
97// are comma-free by construction (see assertAccountKey).
98func ExportAccounts(after string, limit int) (keys, next string) {
99 return pageKeys(&accounts, "", "", after, limit)
100}
101
102// AccountCount is how many per-address fields exist, so a snapshot of
103// the store can be checked for completeness. A separate read rather than
104// a fifth value from Sizes, whose shape the snapshot tools already parse.
105func AccountCount() int { return accounts.Size() }
106
107// ExportDomainExtraKeys / ExportNameExtraKeys list a record's extension
108// keys so a snapshot can capture data this realm has no schema for.
109// Keys are comma-free by construction (see assertExtraKey), so a CSV
110// listing is unambiguous.
111func ExportDomainExtraKeys(label, after string, limit int) (keys, next string) {
112 d := mustGetDomain(label)
113 return pageKeys(&d.extra, "", "", after, limit)
114}
115
116// A previous holder's keys are not exported. A snapshot that captured them
117// would carry the seller's payment addresses into a restore as the buyer's.
118func ExportNameExtraKeys(label, domainLabel, after string, limit int) (keys, next string) {
119 n := mustGetName(label, domainLabel)
120 if !isFresh(n, nameTokenID(label, domainLabel)) {
121 return "", ""
122 }
123 return pageKeys(&n.extra, "", "", after, limit)
124}
125
126// KNOWN SNAPSHOT GAP, recorded rather than left to be discovered during
127// an actual recovery: GRC-721 OPERATOR APPROVALS live inside nsnft's
128// token state and are not enumerable
129// through any read this realm exposes. Every other piece of state is
130// reachable — record fields via GetDomainInfo/GetNameInfo, extension keys
131// via the Export*ExtraKeys pagers, reverse resolution via ExportPrimary,
132// globals via ExportGlobalKeys, per-address state via ExportAccounts —
133// but operator approvals are not, so a
134// reconstructed registry will silently lack them and holders would need
135// to re-grant. Treated as acceptable (an approval is a revocable
136// convenience, not ownership) but it must not be assumed complete.
137
138// Sizes let an operator verify a snapshot is complete rather than
139// trusting that pagination terminated correctly.
140func Sizes() (domainCount, nameCount, primaryCount, globalCount int) {
141 return domains.Size(), names.Size(), primary.Size(), globals.Size()
142}