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

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}