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}