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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}