Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions 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_rc1
  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}