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

history.gno

10.99 Kb · 318 lines
  1package memba_notes_v1
  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}