epochs.gno
9.72 Kb · 318 lines
1package memba_notes_v1
2
3import (
4 "bytes"
5 "gno.land/p/samcrew/avl"
6 "strconv"
7 "strings"
8)
9
10type epochRecord struct {
11 Epoch uint32
12 Manifest, Commitment, RevealedKey []byte
13}
14
15var epochs = avl.NewTree()
16
17func epochKey(id [16]byte, epoch uint32) string {
18 s := strconv.FormatUint(uint64(epoch), 10)
19 return idText(id) + ":" + strings.Repeat("0", 10-len(s)) + s
20}
21func getEpoch(id [16]byte, epoch uint32) *epochRecord {
22 v, ok := epochs.Get(epochKey(id, epoch))
23 if !ok {
24 panic("notes: epoch missing")
25 }
26 return v.(*epochRecord)
27}
28func privateWritable(n *note) {
29 if encrypted(n.Mode) && len(getEpoch(n.ID, n.Epoch).RevealedKey) != 0 {
30 panic("notes: revealed epoch requires rotation")
31 }
32}
33func setRecipients(n *note, list []recipient) {
34 n.Readers = nil
35 n.Writers = nil
36 for _, r := range list {
37 n.Readers = append(n.Readers, r.Who)
38 if r.Writer && r.Who != n.Owner {
39 n.Writers = append(n.Writers, r.Who)
40 }
41 }
42}
43func initializeEncrypted(n *note, access, kc []byte) {
44 validateCommitment(kc)
45 list := parseManifest(access, 1, n.Owner, n.Mode)
46 validateCipher(n.Title, 1, 1, 1, MaxCipherTitleBytes)
47 validateCipher(n.Body, 1, 2, 1, MaxCipherBodyBytes)
48 n.Epoch = 1
49 n.Commitment = copied(kc)
50 setRecipients(n, list)
51 epochs.Set(epochKey(n.ID, 1), &epochRecord{Epoch: 1, Manifest: copied(access), Commitment: copied(kc)})
52}
53
54// Rotate installs the entire next epoch in one transaction. Historical epoch
55// records stay available until Delete+boundedPurge; rotation never scans them.
56func Rotate(cur realm, id [16]byte, baseRevision uint64, expectedEpoch uint32, mode uint8, title, body, manifest, kc []byte, opID [16]byte) {
57 who := enter(cur, false)
58 defer leave()
59 mutateRotate(cur, who, id, baseRevision, expectedEpoch, mode, title, body, manifest, kc, opID)
60}
61func mutateRotate(cur realm, who address, id [16]byte, baseRevision uint64, expectedEpoch uint32, mode uint8, title, body, manifest, kc []byte, opID [16]byte) {
62 n := live(id)
63 owner(n, who)
64 expected(n, baseRevision)
65 if !encrypted(n.Mode) || !encrypted(mode) {
66 panic("notes: encrypted rotation required")
67 }
68 if expectedEpoch != n.Epoch || n.Epoch == ^uint32(0) {
69 panic("notes: stale/exhausted epoch")
70 }
71 if n.TitleRevision == ^uint64(0) || n.BodyRevision == ^uint64(0) {
72 panic("notes: field revision exhausted")
73 }
74 ep := n.Epoch + 1
75 validateCommitment(kc)
76 if bytes.Equal(kc, n.Commitment) {
77 panic("notes: fresh epoch commitment required")
78 }
79 list := parseManifest(manifest, ep, manifestOwner(n), mode)
80 validateCipher(title, ep, 1, n.TitleRevision+1, MaxCipherTitleBytes)
81 validateCipher(body, ep, 2, n.BodyRevision+1, MaxCipherBodyBytes)
82 operation(n, opID)
83 unindex(n)
84 n.Mode = mode
85 n.Epoch = ep
86 n.Title = copied(title)
87 n.Body = copied(body)
88 n.BodyBytes = len(body)
89 n.TitleRevision++
90 n.BodyRevision++
91 n.Commitment = copied(kc)
92 n.PendingOwner = ""
93 setRecipients(n, list)
94 index(n)
95 epochs.Set(epochKey(id, ep), &epochRecord{Epoch: ep, Manifest: copied(manifest), Commitment: copied(kc)})
96 acknowledgeRequests(n, who, opID)
97 bump(n, who, opID, "rotate")
98}
99
100// ApplyAccess grants access to the CURRENT epoch and its history. Removing a
101// reader/writer or changing link scope requires Rotate, never this entrypoint.
102func ApplyAccess(cur realm, id [16]byte, baseRevision uint64, expectedEpoch uint32, mode uint8, manifest []byte, opID [16]byte) {
103 who := enter(cur, false)
104 defer leave()
105 mutateApplyAccess(cur, who, id, baseRevision, expectedEpoch, mode, manifest, opID)
106}
107func mutateApplyAccess(cur realm, who address, id [16]byte, baseRevision uint64, expectedEpoch uint32, mode uint8, manifest []byte, opID [16]byte) {
108 n := live(id)
109 owner(n, who)
110 expected(n, baseRevision)
111 privateWritable(n)
112 if !encrypted(n.Mode) || expectedEpoch != n.Epoch || !(mode == n.Mode || n.Mode == ModePrivate && mode == ModeShared) {
113 panic("notes: access transition requires rotation")
114 }
115 list := parseManifest(manifest, n.Epoch, manifestOwner(n), mode)
116 for _, a := range n.Readers {
117 found := false
118 for _, r := range list {
119 if r.Who == a {
120 found = true
121 if writer(n, a) && !r.Writer {
122 panic("notes: writer removal requires rotation")
123 }
124 }
125 }
126 if !found {
127 panic("notes: reader removal requires rotation")
128 }
129 }
130 operation(n, opID)
131 unindex(n)
132 n.Mode = mode
133 setRecipients(n, list)
134 index(n)
135 getEpoch(id, n.Epoch).Manifest = copied(manifest)
136 acknowledgeRequests(n, who, opID)
137 bump(n, who, opID, "access")
138}
139func Publish(cur realm, id [16]byte, baseRevision uint64, mode uint8, title, body []byte, opID [16]byte) {
140 who := enter(cur, false)
141 defer leave()
142 mutatePublish(cur, who, id, baseRevision, mode, title, body, opID)
143}
144func mutatePublish(cur realm, who address, id [16]byte, baseRevision uint64, mode uint8, title, body []byte, opID [16]byte) {
145 n := live(id)
146 owner(n, who)
147 expected(n, baseRevision)
148 if !encrypted(n.Mode) || !public(mode) {
149 panic("notes: publish transition")
150 }
151 if n.TitleRevision == ^uint64(0) || n.BodyRevision == ^uint64(0) {
152 panic("notes: field revision exhausted")
153 }
154 validatePublic(title, body)
155 operation(n, opID)
156 unindex(n)
157 n.Mode = mode
158 n.Title = copied(title)
159 n.Body = copied(body)
160 n.BodyBytes = len(body)
161 n.TitleRevision++
162 n.BodyRevision++
163 n.Listed = true
164 n.Readers = nil
165 n.Commitment = nil
166 n.PendingOwner = ""
167 index(n)
168 bump(n, who, opID, "publish")
169}
170func Reveal(cur realm, id [16]byte, baseRevision uint64, epoch uint32, key []byte, opID [16]byte) {
171 who := enter(cur, false)
172 defer leave()
173 mutateReveal(cur, who, id, baseRevision, epoch, key, opID)
174}
175func mutateReveal(cur realm, who address, id [16]byte, baseRevision uint64, epoch uint32, key []byte, opID [16]byte) {
176 n := live(id)
177 owner(n, who)
178 expected(n, baseRevision)
179 ep := getEpoch(id, epoch)
180 if len(key) != 32 || len(ep.RevealedKey) != 0 || !bytes.Equal(epochCommitment(id, epoch, key), ep.Commitment) {
181 panic("notes: invalid/already revealed key")
182 }
183 operation(n, opID)
184 ep.RevealedKey = copied(key)
185 bumpHistory(n, who, opID, "reveal", `,"target_epoch":` + u(uint64(epoch)))
186}
187func epochJSON(ep *epochRecord, withManifest bool) string {
188 out := `{"epoch":` + u(uint64(ep.Epoch)) + `,"commitment":` + blob(ep.Commitment) + `,"revealed_key":` + blob(ep.RevealedKey)
189 if withManifest {
190 out += `,"manifest":` + blob(ep.Manifest)
191 }
192 return out + `}`
193}
194func EpochJSON(idHex string, epoch uint32) string {
195 id := readID(idHex)
196 v, ok := epochs.Get(epochKey(id, epoch))
197 if !ok {
198 return "null"
199 }
200 return epochJSON(v.(*epochRecord), true)
201}
202func EpochsJSON(idHex string, cursor uint32, limit int) string {
203 id := readID(idHex)
204 find(id)
205 if limit < 1 || limit > MaxPage {
206 panic("notes: page limit")
207 }
208 items := []string{}
209 var next uint32
210 epochs.Iterate(epochKey(id, cursor), idText(id)+":\xff", func(k string, v interface{}) bool {
211 ep := v.(*epochRecord)
212 if ep.Epoch <= cursor {
213 return false
214 }
215 items = append(items, epochJSON(ep, false))
216 next = ep.Epoch
217 return len(items) == limit
218 })
219 if len(items) < limit {
220 next = 0
221 }
222 return `{"items":[` + strings.Join(items, ",") + `],"next_cursor":` + u(uint64(next)) + `}`
223}
224func ReadersJSON(idHex string) string {
225 n := find(readID(idHex))
226 out := "["
227 for i, a := range n.Readers {
228 if i > 0 {
229 out += ","
230 }
231 out += q(string(a))
232 }
233 return out + "]"
234}
235
236func manifestOwner(n *note) address {
237 if n.OwnerIsRealm {
238 return ""
239 }
240 return n.Owner
241}
242
243// Ownership changes do not silently revoke an existing manifest writer.
244// Revocation requires the new owner to rotate; old key knowledge also remains.
245func preserveTransferredWriters(n *note) {
246 data := getEpoch(n.ID, n.Epoch).Manifest
247 n.Writers = nil
248 for i := 0; i < int(data[1]); i++ {
249 off := 2 + i*ManifestEntryBytes
250 if data[off+20] == 1 {
251 who := manifestAddress(data[off : off+20])
252 if who != n.Owner {
253 n.Writers = append(n.Writers, who)
254 }
255 }
256 }
257}
258
259func WrapJSON(idHex string, epoch uint32, who address) string {
260 id := readID(idHex)
261 who = canonicalRecipient(who)
262 v, ok := epochs.Get(epochKey(id, epoch))
263 if !ok {
264 return "null"
265 }
266 data := v.(*epochRecord).Manifest
267 for i := 0; i < int(data[1]); i++ {
268 off := 2 + i*ManifestEntryBytes
269 if manifestAddress(data[off:off+20]) == who {
270 return wrapJSON(data[off:off+ManifestEntryBytes], epoch, who)
271 }
272 }
273 return "null"
274}
275
276// RefreshOwnWrap changes only the caller's identity envelope. It cannot change
277// roles, readers, modes or epochs, and never rewrites anybody else's recovery.
278func RefreshOwnWrap(cur realm, id [16]byte, baseRevision uint64, expectedEpoch uint32, manifest []byte, opID [16]byte) {
279 who := enter(cur, false)
280 defer leave()
281 n := live(id)
282 expected(n, baseRevision)
283 privateWritable(n)
284 if !cur.Previous().IsUserCall() || !encrypted(n.Mode) || expectedEpoch != n.Epoch || !contains(n.Readers, who) {
285 panic("notes: current reader required")
286 }
287 // A reader who accepted Shared ownership may retain a false explicit writer
288 // flag: ownership is implicit authority. Self-refresh preserves that flag.
289 manifestAccountOwner := address("")
290 if n.Mode == ModePrivate {
291 manifestAccountOwner = n.Owner
292 }
293 parseManifestFor(manifest, n.Epoch, manifestAccountOwner, n.Mode, who)
294 old := getEpoch(id, n.Epoch).Manifest
295 if len(old) != len(manifest) {
296 panic("notes: self refresh cannot change readers")
297 }
298 changed := false
299 for i := 0; i < int(old[1]); i++ {
300 off := 2 + i*ManifestEntryBytes
301 previous := old[off : off+ManifestEntryBytes]
302 next := manifest[off : off+ManifestEntryBytes]
303 if manifestAddress(previous[:20]) == who {
304 if string(previous[:21]) != string(next[:21]) {
305 panic("notes: self refresh cannot change role")
306 }
307 changed = string(previous) != string(next)
308 } else if string(previous) != string(next) {
309 panic("notes: self refresh cannot change another wrap")
310 }
311 }
312 if !changed {
313 panic("notes: wrap unchanged")
314 }
315 operation(n, opID)
316 getEpoch(id, n.Epoch).Manifest = copied(manifest)
317 bump(n, who, opID, "self-rekey")
318}