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

ref.gno

7.88 Kb · 264 lines
  1package forge
  2
  3import (
  4	"crypto/sha256"
  5	"encoding/hex"
  6	"strconv"
  7	"strings"
  8)
  9
 10// Log entry kinds.
 11const (
 12	KindCreate = "create" // a ref that did not exist now points somewhere
 13	KindUpdate = "update" // compare-and-swap succeeded
 14	KindForce  = "force"  // the tip was replaced without a matching expectation
 15	KindDelete = "delete" // the ref is gone (the log is not)
 16	KindMerge  = "merge"  // an update performed by merging a change request
 17)
 18
 19// Ref is the current state of one reference. The history of how it got here is
 20// in the repo log, which nothing can rewrite.
 21type Ref struct {
 22	Name      string
 23	OID       string
 24	UpdatedAt int64
 25	UpdatedBy address
 26}
 27
 28// LogEntry is one link of the repo's reference log. The log is append-only and
 29// hash-chained: Digest commits to every earlier entry, so publishing a single
 30// digest (in a release note, a package manifest, a tweet) pins the entire
 31// history of every ref up to that point.
 32type LogEntry struct {
 33	Seq      int64
 34	Ref      string
 35	OldOID   string // "" when the ref did not exist
 36	NewOID   string // "" on delete
 37	Actor    address
 38	Height   int64
 39	Kind     string
 40	ChangeID int64 // the merged change, 0 otherwise
 41	Note     string
 42	Digest   string // hex sha256 over the previous digest and this entry
 43}
 44
 45// EntryDigest computes the chain digest of e given the previous entry's digest.
 46// It is exported so an off-chain verifier can recompute the chain byte for byte
 47// from the values it read back; the field order below is the wire format and
 48// must not change within a version.
 49func EntryDigest(prev string, e *LogEntry) string {
 50	fields := []string{
 51		prev,
 52		strconv.FormatInt(e.Seq, 10),
 53		e.Ref,
 54		e.OldOID,
 55		e.NewOID,
 56		e.Actor.String(),
 57		strconv.FormatInt(e.Height, 10),
 58		e.Kind,
 59		strconv.FormatInt(e.ChangeID, 10),
 60		e.Note,
 61	}
 62	sum := sha256.Sum256([]byte(strings.Join(fields, "\n")))
 63	return hex.EncodeToString(sum[:])
 64}
 65
 66// appendLog writes one entry and advances the chain head. Callers have already
 67// authorized and validated; this never fails.
 68func (r *Repo) appendLog(actor address, height int64, ref, oldOID, newOID, kind string, changeID int64, note string) *LogEntry {
 69	e := &LogEntry{
 70		Seq:      r.nextSeq,
 71		Ref:      ref,
 72		OldOID:   oldOID,
 73		NewOID:   newOID,
 74		Actor:    actor,
 75		Height:   height,
 76		Kind:     kind,
 77		ChangeID: changeID,
 78		Note:     note,
 79	}
 80	e.Digest = EntryDigest(r.head, e)
 81	r.log.Set(seqKey(e.Seq), e)
 82	r.head = e.Digest
 83	r.nextSeq++
 84	return e
 85}
 86
 87// SetRef moves a ref by compare-and-swap: expectedOID must be the tip the
 88// caller last saw ("" to create a ref that does not exist yet). This is git's
 89// --force-with-lease, except the lease is held by consensus rather than by the
 90// server you are pushing to, so a concurrent push cannot be silently lost and a
 91// rewritten history cannot be presented as if it had always been that way.
 92//
 93// The chain cannot check that newOID descends from expectedOID: it has no
 94// objects. That check belongs to a client holding the repo, which is exactly
 95// why every move is recorded rather than merely applied.
 96func (r *Repo) SetRef(actor address, height int64, name, expectedOID, newOID, note string) (*LogEntry, error) {
 97	if r.Archived {
 98		return nil, ErrRepoArchived
 99	}
100	if !r.Can(actor, RoleWriter) {
101		return nil, ErrUnauthorized
102	}
103	if !ValidRefName(name) {
104		return nil, ErrInvalidRefName
105	}
106	if !ValidOID(newOID) {
107		return nil, ErrInvalidOID
108	}
109	if !ValidLine(note, MaxNoteLen) {
110		return nil, ErrInvalidText
111	}
112	cur := r.Ref(name)
113	switch {
114	case cur == nil && expectedOID != "":
115		return nil, ErrRefNotFound
116	case cur != nil && expectedOID == "":
117		return nil, ErrRefExists
118	case cur != nil && cur.OID != expectedOID:
119		return nil, ErrStaleRef
120	case cur != nil && cur.OID == newOID:
121		return nil, ErrSameOID
122	}
123	kind := KindUpdate
124	old := ""
125	if cur == nil {
126		kind = KindCreate
127	} else {
128		old = cur.OID
129	}
130	r.refs.Set(name, &Ref{Name: name, OID: newOID, UpdatedAt: height, UpdatedBy: actor})
131	return r.appendLog(actor, height, name, old, newOID, kind, 0, note), nil
132}
133
134// ForceSetRef moves a ref without an expectation. It needs RoleMaintainer and
135// is permanently recorded as KindForce: the point is not to forbid a force-push
136// (sometimes it is the right call) but to make one impossible to hide.
137func (r *Repo) ForceSetRef(actor address, height int64, name, newOID, note string) (*LogEntry, error) {
138	if r.Archived {
139		return nil, ErrRepoArchived
140	}
141	if !r.Can(actor, RoleMaintainer) {
142		return nil, ErrUnauthorized
143	}
144	if !ValidRefName(name) {
145		return nil, ErrInvalidRefName
146	}
147	if !ValidOID(newOID) {
148		return nil, ErrInvalidOID
149	}
150	if !ValidLine(note, MaxNoteLen) {
151		return nil, ErrInvalidText
152	}
153	old := ""
154	if cur := r.Ref(name); cur != nil {
155		if cur.OID == newOID {
156			return nil, ErrSameOID
157		}
158		old = cur.OID
159	}
160	r.refs.Set(name, &Ref{Name: name, OID: newOID, UpdatedAt: height, UpdatedBy: actor})
161	return r.appendLog(actor, height, name, old, newOID, KindForce, 0, note), nil
162}
163
164// DeleteRef removes a ref by compare-and-swap. The ref disappears from the
165// current state; the log keeps every object it ever pointed at.
166//
167// Maintainer-only for an economic reason as much as a safety one: on gno.land
168// the storage-deposit refund goes to whoever frees the bytes, not to whoever
169// paid for them (`receiver := caller` in the vm keeper's deposit path, gno
170// master 2026-09-19), so an open delete path pays for vandalism.
171func (r *Repo) DeleteRef(actor address, height int64, name, expectedOID, note string) (*LogEntry, error) {
172	if r.Archived {
173		return nil, ErrRepoArchived
174	}
175	if !r.Can(actor, RoleMaintainer) {
176		return nil, ErrUnauthorized
177	}
178	if !ValidLine(note, MaxNoteLen) {
179		return nil, ErrInvalidText
180	}
181	cur := r.Ref(name)
182	if cur == nil {
183		return nil, ErrRefNotFound
184	}
185	if cur.OID != expectedOID {
186		return nil, ErrStaleRef
187	}
188	if name == r.DefaultRef {
189		return nil, ErrUnauthorized // the default branch is not deletable
190	}
191	r.refs.Remove(name)
192	return r.appendLog(actor, height, name, cur.OID, "", KindDelete, 0, note), nil
193}
194
195// Ref returns the current state of a ref, or nil.
196func (r *Repo) Ref(name string) *Ref {
197	v := r.refs.Get(name)
198	if v == nil {
199		return nil
200	}
201	return v.(*Ref)
202}
203
204// IterateRefs walks refs in name order.
205func (r *Repo) IterateRefs(cb func(*Ref) bool) {
206	r.refs.Iterate("", "", func(_ string, value any) bool {
207		return cb(value.(*Ref))
208	})
209}
210
211// LogHead is the digest of the last entry, "" for an empty log. Pin this value
212// anywhere off chain and the whole history becomes falsifiable.
213func (r *Repo) LogHead() string { return r.head }
214
215// LogSize is the number of entries ever appended.
216func (r *Repo) LogSize() int { return r.log.Size() }
217
218// LogEntryAt returns one entry by sequence number, or nil.
219func (r *Repo) LogEntryAt(seq int64) *LogEntry {
220	v := r.log.Get(seqKey(seq))
221	if v == nil {
222		return nil
223	}
224	return v.(*LogEntry)
225}
226
227// IterateLog walks the log oldest-first.
228func (r *Repo) IterateLog(offset, count int, cb func(*LogEntry) bool) {
229	if count <= 0 {
230		count = r.log.Size()
231	}
232	r.log.IterateByOffset(offset, count, func(_ string, value any) bool {
233		return cb(value.(*LogEntry))
234	})
235}
236
237// IterateLogReverse walks the log newest-first, which is what a UI wants.
238func (r *Repo) IterateLogReverse(offset, count int, cb func(*LogEntry) bool) {
239	if count <= 0 {
240		count = r.log.Size()
241	}
242	r.log.ReverseIterateByOffset(offset, count, func(_ string, value any) bool {
243		return cb(value.(*LogEntry))
244	})
245}
246
247// VerifyLog recomputes the whole digest chain and reports the first entry whose
248// digest does not follow from its predecessor. It should be impossible on a
249// live chain: it is here because a transparency log nobody can verify is just
250// a log, and a client should be running this against the values it read back.
251func (r *Repo) VerifyLog() (bool, int64) {
252	prev := ""
253	bad := int64(-1)
254	r.log.Iterate("", "", func(_ string, value any) bool {
255		e := value.(*LogEntry)
256		if EntryDigest(prev, e) != e.Digest {
257			bad = e.Seq
258			return true
259		}
260		prev = e.Digest
261		return false
262	})
263	return bad < 0, bad
264}