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

history.gno

10.99 Kb · 318 lines
  1package memba_notes_rc1
  2
  3import (
  4	"chain/runtime"
  5	"crypto/sha256"
  6	"encoding/hex"
  7	"gno.land/p/samcrew/avl"
  8	"strconv"
  9	"strings"
 10)
 11
 12// Bytes shares the note's committed array: fields are replaced, never mutated in
 13// place, so history stores each version once and a later commit frees nothing
 14// another account paid for. A moderator redaction clears Bytes and keeps Hash.
 15type historyBlob struct {
 16	Bytes    []byte
 17	Hash     string
 18	Redacted bool
 19}
 20
 21// historyVersion is the public state recorded by one note-level event. It keeps
 22// only what the public history reads expose, never a copy of the live note: no
 23// private manifest, readers, commitment or receipts. Versions that leave a field
 24// unchanged share that field's blob.
 25type historyVersion struct {
 26	Revision, TitleRevision, BodyRevision, OwnerGeneration uint64
 27	Owner, PendingOwner                                    address
 28	Writers                                                []address
 29	PolicyRead, PolicyWrite, PolicyComment                 string
 30	Epoch                                                  uint32
 31	Mode                                                   uint8
 32	OwnerIsRealm, GovWriters, Listed, ModeratorHidden      bool
 33	Deleted, PublicWrites                                  bool
 34	Title, Body                                            *historyBlob
 35}
 36
 37// historyInfo is kept on the public note itself (note.History); the note record
 38// is retained forever as a tombstone, so it needs no index of its own.
 39type historyInfo struct {
 40	StartRevision, StartBodyRevision, Head uint64
 41	Latest                                 *historyVersion
 42}
 43
 44// One record per event. Operation and Comment hold the raw 16-byte ids ("" for
 45// none). Version is set on note-level events only; comment and cleanup events
 46// reuse the current state revision and record no version.
 47type historyEntry struct {
 48	Seq, Revision, Proposal          uint64
 49	Height                           int64
 50	Actor, Executor                  address
 51	Operation, Comment               string
 52	Action, GovernanceAction, Detail string
 53	Version                          *historyVersion
 54}
 55
 56// historyComment keeps the first public version of a comment (Parent to Body,
 57// Parent as a raw id or "") and its latest presentation. Anchor and Body share
 58// the live comment's arrays, so a comment's text is stored once.
 59type historyComment struct {
 60	Parent                       string
 61	Author                       address
 62	BodyRevision                 uint64
 63	Epoch                        uint32
 64	CreatedHeight                int64
 65	Anchor, Body                 []byte
 66	AnchorHistory, ParentHistory bool
 67	LatestSeq                    uint64
 68	Deleted, Hidden, Resolved    bool
 69	Redacted                     bool
 70}
 71type historyCall struct {
 72	Executor  address
 73	Proposal  uint64
 74	Operation [16]byte
 75	Action    string
 76}
 77
 78var historyEntries = avl.NewTree()
 79var historyComments = avl.NewTree()
 80
 81// Only GovApply sets this while the existing busy guard excludes reentry.
 82var historyGov *historyCall
 83
 84func historyKey(id [16]byte, seq uint64) string {
 85	s := strconv.FormatUint(seq, 10)
 86	return idText(id) + ":" + strings.Repeat("0", 20-len(s)) + s
 87}
 88func historyID(text string) [16]byte {
 89	id := readID(text)
 90	if id == ([16]byte{}) {
 91		panic("notes: empty history id")
 92	}
 93	return id
 94}
 95func historyGet(id [16]byte) *historyInfo {
 96	v, ok := notes.Get(idText(id))
 97	if !ok {
 98		return nil
 99	}
100	return v.(*note).History
101}
102func historyEntryAt(id [16]byte, seq uint64) *historyEntry {
103	v, ok := historyEntries.Get(historyKey(id, seq))
104	if !ok {
105		panic("notes: history gap")
106	}
107	return v.(*historyEntry)
108}
109
110// historyVersionAt returns the version recorded by the event with this seq, or
111// nil when there is no such event or it is a comment/cleanup event.
112func historyVersionAt(id [16]byte, seq uint64) *historyVersion {
113	v, ok := historyEntries.Get(historyKey(id, seq))
114	if !ok {
115		return nil
116	}
117	return v.(*historyEntry).Version
118}
119
120// historyRevisionEntry finds the note-level event of a public state revision
121// without an index. Entries are contiguous from seq 1 and their state revision
122// never decreases; each state revision since publication is recorded by exactly
123// one note-level event, the first entry carrying it.
124func historyRevisionEntry(id [16]byte, h *historyInfo, revision uint64) *historyEntry {
125	if revision < h.StartRevision || revision > h.Latest.Revision {
126		return nil
127	}
128	lo, hi := uint64(1), h.Head
129	for lo < hi {
130		mid := lo + (hi-lo)/2
131		if historyEntryAt(id, mid).Revision < revision {
132			lo = mid + 1
133		} else {
134			hi = mid
135		}
136	}
137	e := historyEntryAt(id, lo)
138	if e.Revision != revision || e.Version == nil {
139		panic("notes: corrupt history")
140	}
141	return e
142}
143func historyBlobOf(data []byte) *historyBlob {
144	hash := sha256.Sum256(data)
145	return &historyBlob{Bytes: data, Hash: hex.EncodeToString(hash[:])}
146}
147func rawID(id [16]byte) string {
148	if id == ([16]byte{}) {
149		return ""
150	}
151	return string(id[:])
152}
153func historyNullableRaw(raw string) string {
154	if raw == "" {
155		return "null"
156	}
157	return q(hex.EncodeToString([]byte(raw)))
158}
159func historyAppend(n *note, who address, op, cid [16]byte, action, detail string) *historyEntry {
160	h := n.History
161	if h == nil || h.Head == ^uint64(0) {
162		panic("notes: history unavailable/exhausted")
163	}
164	h.Head++
165	e := &historyEntry{Seq: h.Head, Revision: n.Revision, Height: runtime.ChainHeight(), Actor: who, Operation: rawID(op), Comment: rawID(cid), Action: action, Detail: detail}
166	if historyGov != nil {
167		e.Executor, e.Proposal, e.GovernanceAction = historyGov.Executor, historyGov.Proposal, historyGov.Action
168		e.Operation = rawID(historyGov.Operation)
169	}
170	if _, exists := historyEntries.Get(historyKey(n.ID, e.Seq)); exists {
171		panic("notes: history collision")
172	}
173	historyEntries.Set(historyKey(n.ID, e.Seq), e)
174	return e
175}
176func historyNote(n *note, who address, op [16]byte, action, extra string) {
177	if !public(n.Mode) {
178		return
179	}
180	h := n.History
181	if h == nil {
182		if action != "create" && action != "publish" {
183			panic("notes: missing public history")
184		}
185		h = &historyInfo{StartRevision: n.Revision, StartBodyRevision: n.BodyRevision}
186		n.History = h
187	}
188	previous := "null"
189	mask := 0
190	if h.Latest != nil {
191		previous = u(h.Latest.Revision)
192		if n.TitleRevision != h.Latest.TitleRevision {
193			mask |= 1
194		}
195		if n.BodyRevision != h.Latest.BodyRevision {
196			mask |= 2
197		}
198	}
199	switch action {
200	case "commit":
201		action = "content-commit"
202		extra = `,"field_mask":` + strconv.Itoa(mask)
203	case "comments-mode":
204		action = "comment-mode"
205	case "policy":
206		action = "policy-set"
207	case "listing":
208		action = "owner-listing"
209	case "moderate-listing":
210		action = "listing-moderation"
211	case "delete":
212		action = "note-delete"
213	case "reveal":
214		action = "legacy-epoch-reveal"
215	case "owner-propose":
216		if n.PendingOwner == "" {
217			action = "owner-cancel"
218		}
219	}
220	e := historyAppend(n, who, op, [16]byte{}, action, `{"snapshot_revision":`+u(n.Revision)+`,"previous_state_revision":`+previous+extra+`}`)
221	v := &historyVersion{Revision: n.Revision, TitleRevision: n.TitleRevision, BodyRevision: n.BodyRevision, OwnerGeneration: n.OwnerGeneration,
222		Owner: n.Owner, PendingOwner: n.PendingOwner, Writers: historyWriters(h.Latest, n.Writers),
223		PolicyRead: n.Policies[0], PolicyWrite: n.Policies[1], PolicyComment: n.Policies[2], Epoch: n.Epoch, Mode: n.Mode,
224		OwnerIsRealm: n.OwnerIsRealm, GovWriters: n.GovWriters, Listed: n.Listed, ModeratorHidden: n.ModeratorHidden,
225		Deleted: n.Deleted, PublicWrites: publicWritesEnabled(n)}
226	if !n.Deleted {
227		if h.Latest != nil && h.Latest.TitleRevision == n.TitleRevision {
228			v.Title = h.Latest.Title
229		} else {
230			v.Title = historyBlobOf(n.Title)
231		}
232		if h.Latest != nil && h.Latest.BodyRevision == n.BodyRevision {
233			v.Body = h.Latest.Body
234		} else {
235			v.Body = historyBlobOf(n.Body)
236		}
237	}
238	e.Version = v
239	h.Latest = v
240}
241
242// historyWriters shares the previous version's writer list while it is
243// unchanged. Recorded lists are never mutated, and the live list is never
244// shared: a changed list is copied, since RemoveWriter edits it in place.
245func historyWriters(latest *historyVersion, writers []address) []address {
246	if latest != nil && len(latest.Writers) == len(writers) {
247		same := true
248		for i, who := range writers {
249			if latest.Writers[i] != who {
250				same = false
251				break
252			}
253		}
254		if same {
255			return latest.Writers
256		}
257	}
258	return copiedAddresses(writers)
259}
260func historyNullableText(s string) string {
261	if s == "" {
262		return "null"
263	}
264	return q(s)
265}
266
267// historyCommentDetail renders a comment reference; cid is the hex id and parent
268// the raw parent id ("" for a top-level comment).
269func historyCommentDetail(cid, parent string, revision, bodyRevision uint64, deleted, hidden, resolved bool, archived *historyComment) string {
270	anchorRevision := "null"
271	if archived.AnchorHistory {
272		anchorRevision = u(bodyRevision)
273	}
274	return `{"comment_id":` + q(cid) + `,"comment_revision":` + u(revision) + `,"content_ref":` + q(cid) + `,"body_revision":` + u(bodyRevision) + `,"anchor_history_body_revision":` + anchorRevision + `,"parent":` + historyNullableRaw(parent) + `,"parent_history_available":` + strconv.FormatBool(archived.ParentHistory) + `,"deleted":` + strconv.FormatBool(deleted) + `,"hidden":` + strconv.FormatBool(hidden) + `,"resolved":` + strconv.FormatBool(resolved) + `}`
275}
276func historyCommentChange(n *note, c *comment, action string) {
277	if !public(n.Mode) || c.Encrypted {
278		return
279	}
280	key := commentKey(n.ID, c.ID)
281	value, exists := historyComments.Get(key)
282	var archived *historyComment
283	if action == "comment-add" {
284		if exists {
285			panic("notes: comment history collision")
286		}
287		h := n.History
288		if h == nil {
289			panic("notes: missing public history")
290		}
291		// Every body revision since publication is recorded, so the anchored
292		// version is in public history exactly when it is not older than that.
293		anchor := c.BodyRevision >= h.StartBodyRevision
294		_, parent := historyComments.Get(commentKey(n.ID, c.Parent))
295		// Anchor and Body share the comment's arrays for the same single-copy reason.
296		archived = &historyComment{Parent: rawID(c.Parent), Author: c.Author, BodyRevision: c.BodyRevision, Epoch: c.Epoch, CreatedHeight: c.CreatedHeight,
297			Anchor: c.Anchor, Body: c.Body, AnchorHistory: anchor, ParentHistory: parent}
298		historyComments.Set(key, archived)
299	} else {
300		if !exists {
301			panic("notes: missing public comment history")
302		}
303		archived = value.(*historyComment)
304	}
305	e := historyAppend(n, c.Last.Actor, c.Last.Operation, c.ID, action, historyCommentDetail(idText(c.ID), rawID(c.Parent), c.Revision, c.BodyRevision, c.Deleted, c.Hidden, c.Resolved, archived))
306	archived.LatestSeq, archived.Deleted, archived.Hidden, archived.Resolved = e.Seq, c.Deleted, c.Hidden, c.Resolved
307}
308func historyCleanup(n *note, who address, counts [5]int) {
309	if !public(n.Mode) {
310		return
311	}
312	total := counts[0] + counts[1] + counts[2] + counts[3] + counts[4]
313	if total == 0 {
314		return
315	}
316	detail := `{"comment_index":` + u(uint64(counts[0])) + `,"comments":` + u(uint64(counts[1])) + `,"cooldowns":` + u(uint64(counts[2])) + `,"epochs":` + u(uint64(counts[3])) + `,"requests":` + u(uint64(counts[4])) + `,"total":` + u(uint64(total)) + `}`
317	historyAppend(n, who, [16]byte{}, [16]byte{}, "cleanup", detail)
318}