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.

sugarproxy.gno

14.37 Kb · 494 lines
  1// Package sugarproxy holds the rules of sugar proxy (sugar repo, PROXY.md): which sites sharers' home connections
  2// may carry, for which customers and why; which app releases they may run; which hubs they may connect to; who pays
  3// them. Sharers' apps read these rules through a light client, from one parameter this realm writes on every change
  4// (params "doc": the canonical JSON of everything below), so no hub can change what a sharer's connection does.
  5//
  6// Who decides:
  7//   - Holders of the SUGAR token, through kourt's governor (govern.gno): they add and remove anything.
  8//   - A delegate realm, if they name one (gno.security's, say): it adds and removes sites, nothing else.
  9//   - GovDAO (govdao.gno), above both: it adds and removes anything at once, vetoes, pauses, and names the delegate
 10//     and the guardian. A guardian it names (the deployer, at first) may veto and pause, never approve.
 11//
 12// Anything added by the token or the delegate waits out a veto window before it counts; GovDAO's own additions
 13// count at once. A veto or a removal counts at once, whoever makes it.
 14package sugarproxy
 15
 16import (
 17	"chain"
 18	"chain/params"
 19	"crypto/sha256"
 20	"encoding/hex"
 21	"strconv"
 22	"strings"
 23	"time"
 24
 25	"gno.land/p/nt/ufmt/v0"
 26)
 27
 28func errorf(format string, args ...any) error { return ufmt.Errorf(format, args...) }
 29
 30// Item is one entry of the rules: a site, a release, a hub, a customer or a payer.
 31type Item struct {
 32	ID       int
 33	Kind     string // KindSite, KindRelease, KindHub, KindCustomer, KindPayer
 34	Fields   []string
 35	State    string // StateApproved, StateVetoed, StateRevoked
 36	By       string // who added it: "init", "token", "delegate", "govdao"
 37	Approved int64  // unix seconds
 38	After    int64  // unix seconds: when its veto window ends and it counts
 39	Note     string // who vetoed or removed it, and when
 40}
 41
 42const (
 43	KindSite     = "site"     // fields: pattern, customer, purpose
 44	KindRelease  = "release"  // fields: version, platform, sha256, url
 45	KindHub      = "hub"      // fields: host:port, pin (hex sha256 of its certificate's public key)
 46	KindCustomer = "customer" // fields: name, purpose
 47	KindPayer    = "payer"    // fields: address (may call Pay)
 48
 49	StateApproved = "approved"
 50	StateVetoed   = "vetoed"
 51	StateRevoked  = "revoked"
 52)
 53
 54var (
 55	items     []*Item
 56	nextID    = 1
 57	paused    bool
 58	delegate  string  // the realm path that may add and remove sites; "" is none
 59	guardian  address // may veto and pause; GovDAO names it
 60	vetoWin   = map[string]int64{KindSite: 3 * 86400, KindRelease: 7 * 86400, KindHub: 3 * 86400, KindCustomer: 3 * 86400, KindPayer: 3 * 86400}
 61	doc       string // the canonical JSON the apps read, as params "doc"
 62	digest    string // its hex sha256
 63	docSeq    int    // how many times it has changed
 64	requests  []*Request
 65	nextReqID = 1
 66	launching bool // launch.gno: while the first install's items go in
 67)
 68
 69// Request is anyone's suggestion of an item, for the token holders or the delegate to consider. It changes nothing.
 70type Request struct {
 71	ID      int
 72	By      address
 73	Payload string
 74	Reason  string
 75	Time    int64
 76	Done    bool
 77}
 78
 79func init(cur realm) {
 80	setupGovernor()
 81	// The owner (launch.gno; the deployer when unset) is the first guardian (GovDAO can replace it) and holds the
 82	// whole first supply of SUGAR (govern.gno).
 83	who := owner
 84	if who == "" {
 85		who = cur.Previous().Address()
 86	}
 87	if who != "" {
 88		guardian = who
 89		mintInitial(who)
 90	}
 91	launch()
 92	publish()
 93}
 94
 95// --- what an item may say ---
 96
 97// parse turns "kind|field|field..." into an item's kind and fields, checking each field.
 98func parse(payload string) (string, []string, error) {
 99	parts := strings.Split(payload, "|")
100	kind, f := parts[0], parts[1:]
101	want := map[string]int{KindSite: 3, KindRelease: 4, KindHub: 2, KindCustomer: 2, KindPayer: 1}[kind]
102	if want == 0 {
103		return "", nil, errorf("unknown kind %q: site, release, hub, customer or payer", kind)
104	}
105	if len(f) != want {
106		return "", nil, errorf("a %s has %d fields, separated by |", kind, want)
107	}
108	for _, x := range f {
109		if len(x) == 0 || len(x) > 300 || strings.ContainsAny(x, "\n\r\t") {
110			return "", nil, errorf("each field is 1 to 300 characters on one line")
111		}
112	}
113	switch kind {
114	case KindSite:
115		if !sitePattern(f[0]) {
116			return "", nil, errorf("%q is not a site: a lowercase DNS name, or *. and one", f[0])
117		}
118		if !customerActive(f[1]) {
119			return "", nil, errorf("no customer %q is approved", f[1])
120		}
121	case KindRelease:
122		if !word(f[0], 1, 32) || !word(f[1], 3, 32) || !hexOf(f[2], 64) || !strings.HasPrefix(f[3], "https://") {
123			return "", nil, errorf("a release is version|platform|sha256 (64 hex)|https://url")
124		}
125	case KindHub:
126		host, port, ok := strings.Cut(f[0], ":")
127		n, err := strconv.Atoi(port)
128		if !ok || !sitePattern(host) || strings.HasPrefix(host, "*.") || err != nil || n < 1 || n > 65535 || !hexOf(f[1], 64) {
129			return "", nil, errorf("a hub is host:port|pin (64 hex)")
130		}
131	case KindCustomer:
132		if !word(f[0], 2, 32) {
133			return "", nil, errorf("a customer's name is 2 to 32 of a-z, 0-9 and -")
134		}
135	case KindPayer:
136		if !address(f[0]).IsValid() {
137			return "", nil, errorf("%q is not an address", f[0])
138		}
139	}
140	return kind, f, nil
141}
142
143func word(s string, min, max int) bool {
144	if len(s) < min || len(s) > max {
145		return false
146	}
147	for i := 0; i < len(s); i++ {
148		c := s[i]
149		if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-' || c == '.' || c == '_') {
150			return false
151		}
152	}
153	return true
154}
155
156func hexOf(s string, n int) bool {
157	if len(s) != n {
158		return false
159	}
160	_, err := hex.DecodeString(s)
161	return err == nil && strings.ToLower(s) == s
162}
163
164// sitePattern is a lowercase DNS name of two or more labels, or "*." and one: what an app matches a host against.
165func sitePattern(s string) bool {
166	s = strings.TrimPrefix(s, "*.")
167	if len(s) > 253 {
168		return false
169	}
170	labels := strings.Split(s, ".")
171	if len(labels) < 2 {
172		return false
173	}
174	for _, l := range labels {
175		if len(l) == 0 || len(l) > 63 || l[0] == '-' || l[len(l)-1] == '-' {
176			return false
177		}
178		for i := 0; i < len(l); i++ {
179			c := l[i]
180			if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-') {
181				return false
182			}
183		}
184	}
185	return strings.Trim(labels[len(labels)-1], "0123456789") != ""
186}
187
188func customerActive(name string) bool {
189	now := time.Now().Unix()
190	for _, it := range items {
191		if it.Kind == KindCustomer && it.Fields[0] == name && it.State == StateApproved && now >= it.After {
192			return true
193		}
194	}
195	return false
196}
197
198// --- changes: every one goes through here and republishes the doc ---
199
200// add puts an item in, approved. by says who: its veto window applies unless GovDAO added it.
201func add(payload, by string) (int, error) {
202	if paused && by != "govdao" {
203		return 0, errorf("sugar proxy is paused")
204	}
205	kind, fields, err := parse(payload)
206	if err != nil {
207		return 0, err
208	}
209	if by == "delegate" && kind != KindSite {
210		return 0, errorf("the delegate adds sites only")
211	}
212	for _, it := range items {
213		if it.Kind == kind && strings.Join(it.Fields, "|") == strings.Join(fields, "|") && it.State == StateApproved {
214			return 0, errorf("item %d already says that", it.ID)
215		}
216	}
217	now := time.Now().Unix()
218	after := now + vetoWin[kind]
219	if by == "govdao" || by == "init" || by == "launcher" {
220		after = now // GovDAO is the veto; the first install is signed by Jae; the launcher's sites are removable at once
221	}
222	it := &Item{ID: nextID, Kind: kind, Fields: fields, State: StateApproved, By: by, Approved: now, After: after}
223	nextID++
224	items = append(items, it)
225	for _, r := range requests {
226		if !r.Done && r.Payload == payload {
227			r.Done = true
228		}
229	}
230	chain.Emit("Added", "id", strconv.Itoa(it.ID), "kind", kind, "by", by, "after", strconv.FormatInt(after, 10))
231	publish()
232	return it.ID, nil
233}
234
235// end vetoes or removes an item: a veto only while its window is open, a removal any time. It counts at once.
236func end(id int, veto bool, by string) error {
237	it := find(id)
238	if it == nil {
239		return errorf("no item %d", id)
240	}
241	if it.State != StateApproved {
242		return errorf("item %d is already %s", id, it.State)
243	}
244	now := time.Now().Unix()
245	if veto {
246		if now >= it.After {
247			return errorf("item %d's veto window is over; remove it instead", id)
248		}
249		it.State = StateVetoed
250	} else {
251		it.State = StateRevoked
252	}
253	it.Note = it.State + " by " + by + " at " + strconv.FormatInt(now, 10)
254	chain.Emit("Ended", "id", strconv.Itoa(id), "state", it.State, "by", by)
255	publish()
256	return nil
257}
258
259func setPaused(on bool, by string) {
260	if paused == on {
261		return
262	}
263	paused = on
264	chain.Emit("Paused", "on", strconv.FormatBool(on), "by", by)
265	publish()
266}
267
268func find(id int) *Item {
269	for _, it := range items {
270		if it.ID == id {
271			return it
272		}
273	}
274	return nil
275}
276
277// publish writes the doc the apps read: params "doc", at gno.land's main store key
278// "/pv/vm:<this realm's path>:doc", which a light client proves against a block's app hash.
279func publish() {
280	if launching {
281		return
282	}
283	doc = render()
284	sum := sha256.Sum256([]byte(doc))
285	digest = hex.EncodeToString(sum[:])
286	docSeq++
287	params.SetBytes("doc", []byte(doc))
288}
289
290// render is the canonical JSON of the rules: every item ever approved, in order, with its state, so an app can
291// tell what counts at any time (approved, and its After passed) without trusting anyone's clock but its own.
292func render() string {
293	var b strings.Builder
294	b.WriteString(`{"v":1,"seq":`)
295	b.WriteString(strconv.Itoa(docSeq + 1))
296	b.WriteString(`,"paused":`)
297	b.WriteString(strconv.FormatBool(paused))
298	b.WriteString(`,"items":[`)
299	for i, it := range items {
300		if i > 0 {
301			b.WriteByte(',')
302		}
303		b.WriteString(`{"id":`)
304		b.WriteString(strconv.Itoa(it.ID))
305		b.WriteString(`,"kind":`)
306		b.WriteString(jstr(it.Kind))
307		b.WriteString(`,"fields":[`)
308		for j, f := range it.Fields {
309			if j > 0 {
310				b.WriteByte(',')
311			}
312			b.WriteString(jstr(f))
313		}
314		b.WriteString(`],"state":`)
315		b.WriteString(jstr(it.State))
316		b.WriteString(`,"by":`)
317		b.WriteString(jstr(it.By))
318		b.WriteString(`,"approved":`)
319		b.WriteString(strconv.FormatInt(it.Approved, 10))
320		b.WriteString(`,"after":`)
321		b.WriteString(strconv.FormatInt(it.After, 10))
322		b.WriteByte('}')
323	}
324	b.WriteString(`]}`)
325	return b.String()
326}
327
328// jstr quotes s as a JSON string.
329func jstr(s string) string {
330	const hexd = "0123456789abcdef"
331	var b strings.Builder
332	b.WriteByte('"')
333	for i := 0; i < len(s); i++ {
334		c := s[i]
335		switch {
336		case c == '"':
337			b.WriteString(`\"`)
338		case c == '\\':
339			b.WriteString(`\\`)
340		case c < 0x20:
341			b.WriteString(`\u00`)
342			b.WriteByte(hexd[c>>4])
343			b.WriteByte(hexd[c&15])
344		default:
345			b.WriteByte(c)
346		}
347	}
348	b.WriteByte('"')
349	return b.String()
350}
351
352// --- reading ---
353
354// Doc is the canonical JSON the apps read (the same bytes as params "doc"); Digest is its hex sha256.
355func Doc() string    { return doc }
356func Digest() string { return digest }
357
358// ItemID is the number of the approved item that says exactly payload ("kind|field|..."), or 0.
359func ItemID(payload string) int {
360	for _, it := range items {
361		if it.State == StateApproved && it.Kind+"|"+joinFields(it.Fields) == payload {
362			return it.ID
363		}
364	}
365	return 0
366}
367
368// Paused reports whether sugar proxy is paused: apps carry nothing while it is.
369func Paused() bool { return paused }
370
371// Delegate is the realm that may add and remove sites ("" is none); Guardian may veto and pause.
372func Delegate() string  { return delegate }
373func Guardian() address { return guardian }
374
375// VetoWindow is how long an added item of a kind waits before it counts, in seconds.
376func VetoWindow(kind string) int64 { return vetoWin[kind] }
377
378// --- anyone ---
379
380// Suggest records a suggestion of an item ("kind|field|..."), for the token holders or the delegate to consider. It
381// changes nothing. Each address may have three open at a time.
382func Suggest(cur realm, payload, reason string) int {
383	by := mustUser(cur)
384	if _, _, err := parse(payload); err != nil {
385		panic(err.Error())
386	}
387	if len(reason) > 500 {
388		panic("the reason is at most 500 characters")
389	}
390	open := 0
391	for _, r := range requests {
392		if r.By == by && !r.Done {
393			open++
394		}
395	}
396	if open >= 3 {
397		panic("you have three suggestions open already")
398	}
399	r := &Request{ID: nextReqID, By: by, Payload: payload, Reason: reason, Time: time.Now().Unix()}
400	nextReqID++
401	requests = append(requests, r)
402	chain.Emit("Suggested", "id", strconv.Itoa(r.ID), "by", by.String())
403	return r.ID
404}
405
406// Withdraw closes your own suggestion.
407func Withdraw(cur realm, id int) {
408	by := mustUser(cur)
409	for _, r := range requests {
410		if r.ID == id && r.By == by {
411			r.Done = true
412			return
413		}
414	}
415	panic("no suggestion of yours has that id")
416}
417
418// --- the delegate realm ---
419
420// DelegateAdd adds a site ("site|pattern|customer|purpose"), from the delegate realm only; it waits out the
421// site veto window.
422func DelegateAdd(cur realm, payload string) int {
423	mustDelegate(cur)
424	id, err := add(payload, "delegate")
425	if err != nil {
426		panic(err.Error())
427	}
428	return id
429}
430
431// DelegateRemove removes a site, from the delegate realm only.
432func DelegateRemove(cur realm, id int) {
433	mustDelegate(cur)
434	if it := find(id); it == nil || it.Kind != KindSite {
435		panic("the delegate removes sites only")
436	}
437	if err := end(id, false, "delegate"); err != nil {
438		panic(err.Error())
439	}
440}
441
442// mustDelegate refuses any caller but the delegate realm itself, the way r/sys/params checks its delegates: none
443// named refuses everyone, and the caller's realm path must match exactly.
444func mustDelegate(cur realm) {
445	if delegate == "" {
446		panic("no delegate is named")
447	}
448	if cur.Previous().PkgPath() != delegate {
449		panic("only the delegate realm " + delegate + " may do that")
450	}
451}
452
453// --- the guardian ---
454
455// GuardianVeto vetoes an item still in its veto window.
456func GuardianVeto(cur realm, id int) {
457	mustGuardian(cur)
458	if err := end(id, true, "guardian"); err != nil {
459		panic(err.Error())
460	}
461}
462
463// GuardianRemove removes a site, at once: the safe direction, so the guardian may (the launcher's sites count at
464// once, so there is no window to veto them in).
465func GuardianRemove(cur realm, id int) {
466	mustGuardian(cur)
467	if it := find(id); it == nil || it.Kind != KindSite {
468		panic("the guardian removes sites only")
469	}
470	if err := end(id, false, "guardian"); err != nil {
471		panic(err.Error())
472	}
473}
474
475// GuardianPause pauses or resumes sugar proxy.
476func GuardianPause(cur realm, on bool) {
477	mustGuardian(cur)
478	setPaused(on, "guardian")
479}
480
481func mustGuardian(cur realm) {
482	if guardian == "" || mustUser(cur) != guardian {
483		panic("only the guardian may do that")
484	}
485}
486
487// mustUser returns the caller, who must be a user, not another realm's code.
488func mustUser(cur realm) address {
489	prev := cur.Previous()
490	if !prev.IsUser() {
491		panic("only a user may call this")
492	}
493	return prev.Address()
494}