Transaction
B9807879394C79…AB17A0B69D46
Block 1,243,722 · index 0 · indexed
Summary
- Hash
- B9807879394C797758D79867A7BBF8FAC7BC29C438089074650AAB17A0B69D46
- Block
- 1,243,722
- Size
- 37312 bytes
- Gas used
- 49,423,194 / 400,000,000
- Fee
- 400000 ugnot
- Status
- success
Messages
- Attached funds
- 19000000 ugnot
Arguments · 17
- #1sugarproxy
- #2gnomod.toml
- #3module = "gno.land/r/g1hryvj3upm2c89tk0ge976nvh6dl5ukldkx9r45/sugarproxy" gno = "0.9"
- #4govdao.gno
- #5package sugarproxy import ( "strconv" "gno.land/r/gov/dao" ) // GovDAO's powers over sugar proxy, the last word over everything. Each Propose* below makes a GovDAO proposal // request; a GovDAO member submits it (dao.MustCreateProposal), members vote, and when it executes, GovDAO's // executor calls the sealed callback, which only r/gov/dao can do. A callback checks the state again (it may have // changed since the proposal was made) and returns an error rather than panicking, so a stale proposal fails // cleanly instead of sticking. // ProposeAdd adds an item at once, with no veto window: GovDAO is the veto. func ProposeAdd(cur realm, payload string) dao.ProposalRequest { if _, _, err := parse(payload); err != nil { panic(err.Error()) } cb := func(cur realm) error { _, err := add(payload, "govdao") return err } return dao.NewProposalRequest("sugar proxy: add "+payload, "Adds `"+payload+"` to sugar proxy's rules, counting at once.", dao.NewSimpleExecutor(0, cur, cb, "")) } // ProposeVeto vetoes an item still in its veto window. func ProposeVeto(cur realm, id int) dao.ProposalRequest { cb := func(cur realm) error { return end(id, true, "govdao") } return dao.NewProposalRequest("sugar proxy: veto item "+strconv.Itoa(id), describeItem(id)+" GovDAO vetoes it.", dao.NewSimpleExecutor(0, cur, cb, "")) } // ProposeRemove removes an item at once. func ProposeRemove(cur realm, id int) dao.ProposalRequest { cb := func(cur realm) error { return end(id, false, "govdao") } return dao.NewProposalRequest("sugar proxy: remove item "+strconv.Itoa(id), describeItem(id)+" GovDAO removes it.", dao.NewSimpleExecutor(0, cur, cb, "")) } // ProposePause pauses sugar proxy (on) or resumes it: while paused, apps carry nothing and nothing new is added // except by GovDAO. func ProposePause(cur realm, on bool) dao.ProposalRequest { cb := func(cur realm) error { setPaused(on, "govdao") return nil } what := "resume" if on { what = "pause" } return dao.NewProposalRequest("sugar proxy: "+what, "GovDAO "+what+"s sugar proxy.", dao.NewSimpleExecutor(0, cur, cb, "")) } // ProposeDelegate names the realm that may add and remove sites ("" for none). func ProposeDelegate(cur realm, path string) dao.ProposalRequest { cb := func(cur realm) error { delegate = path publish() return nil } return dao.NewProposalRequest("sugar proxy: delegate sites to "+path, "Names `"+path+"` as the realm that may add and "+ "remove sugar proxy's sites (each waiting out the veto window). Empty names none.", dao.NewSimpleExecutor(0, cur, cb, "")) } // ProposeGuardian names the address that may veto and pause ("" for none). func ProposeGuardian(cur realm, a address) dao.ProposalRequest { if a != "" && !a.IsValid() { panic("not an address") } cb := func(cur realm) error { guardian = a return nil } return dao.NewProposalRequest("sugar proxy: guardian "+a.String(), "Names "+a.String()+" sugar proxy's guardian, who may "+ "veto items in their window and pause, never approve. Empty names none.", dao.NewSimpleExecutor(0, cur, cb, "")) } // ProposeVetoWindow sets how long an added item of a kind waits before it counts, in seconds (one hour to 60 days). func ProposeVetoWindow(cur realm, kind string, seconds int64) dao.ProposalRequest { if _, ok := vetoWin[kind]; !ok || seconds < 3600 || seconds > 60*86400 { panic("a kind's veto window is 3600 to 5184000 seconds") } cb := func(cur realm) error { vetoWin[kind] = seconds return nil } return dao.NewProposalRequest("sugar proxy: "+kind+" veto window "+strconv.FormatInt(seconds, 10)+" s", "Sets the veto window "+ "for a new "+kind+" to "+strconv.FormatInt(seconds, 10)+" seconds.", dao.NewSimpleExecutor(0, cur, cb, "")) } func describeItem(id int) string { it := find(id) if it == nil { return "Item " + strconv.Itoa(id) + " (none such yet)." } return "Item " + strconv.Itoa(id) + ": " + it.Kind + " `" + joinFields(it.Fields) + "`, " + it.State + "." } func joinFields(f []string) string { out := "" for i, x := range f { if i > 0 { out += "|" } out += x } return out }
- #6govern.gno
- #7package sugarproxy import ( "strconv" "strings" governor "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/governor/v0" grc20votes "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/grc20votes/v0" ) // SUGAR, the governance token: kourt's grc20votes ledger (balances checkpointed per epoch of 720 blocks, about an // hour, so a vote weighs what a holder held before the proposal) and kourt's governor over it. Both packages are // deployed under Jae's address on gnoland-1. // // The governor's proposals here are of two kinds of this realm's own: add an item, remove one. Holders propose // with Propose(cur, kind, payload, title), vote with Vote, and anyone may Execute a proposal that passed and // whose delay is over. var ( ledger = grc20votes.NewLedger("sugar proxy", "SUGAR", 6, 720) engine = governor.New(ledger, ledger) ) // InitialSupply is minted to the deployer at init: 1,000,000 SUGAR of 6 decimals. const InitialSupply = int64(1_000_000_000_000) const ( kindAdd = governor.Reserved + "sugar-add" kindRemove = governor.Reserved + "sugar-remove" ) // rules are kourt's bootstrap rules (a fifth of the supply turns out, two thirds of the votes cast say yes, a week // to vote, two days' delay, two weeks to execute), except that proposing takes 0.1% of the supply: kourt's lets // anyone propose. func rules() governor.Rules { r := governor.BootstrapRules() r.ProposeBps = 10 return r } // setupGovernor adopts this realm's two kinds; init calls it. func setupGovernor() { engine.InstallBuiltin(addKind{}, rules()) engine.InstallBuiltin(removeKind{}, rules()) } func mintInitial(to address) { ledger.Mint(to, InitialSupply) } // KindAdd and KindRemove are the names to propose under. func KindAdd() string { return kindAdd } func KindRemove() string { return kindRemove } type addKind struct{} func (addKind) Name() string { return kindAdd } func (addKind) Describe(payload string) string { return "Add to sugar proxy's rules: `" + payload + "`. Once executed it waits out the veto window for its kind, " + "during which GovDAO or the guardian may veto it." } func (addKind) Check(payload string) error { _, _, err := parse(payload) return err } func (addKind) Do(_ int, _ realm, payload string) error { _, err := add(payload, "token") return err } type removeKind struct{} func (removeKind) Name() string { return kindRemove } func (removeKind) Describe(payload string) string { return "Remove item " + payload + " from sugar proxy's rules, at once." } func (removeKind) Check(payload string) error { id, err := strconv.Atoi(strings.TrimSpace(payload)) if err != nil { return errorf("the payload is an item's number") } if it := find(id); it == nil || it.State != StateApproved { return errorf("item %d isn't approved", id) } return nil } func (removeKind) Do(_ int, _ realm, payload string) error { id, _ := strconv.Atoi(strings.TrimSpace(payload)) return end(id, false, "token") } const errStaleRealm = "sugarproxy: that realm value is not live; pass your own cur" func live(cur realm) { if !cur.IsCurrent() { panic(errStaleRealm) } } // Propose opens a proposal: kind is KindAdd() with a payload "kind|field|...", or KindRemove() with an item's // number. It needs the governor's share of the supply to propose. func Propose(cur realm, kind, payload, title string) int64 { live(cur) return engine.Propose(cur.Previous().Address(), kind, payload, title) } // Vote casts the caller's weight: "yes", "no" or "abstain". func Vote(cur realm, id int64, choice string) { live(cur) engine.Vote(cur.Previous().Address(), id, choice) } // Execute carries out a proposal that passed, once its delay is over. func Execute(cur realm, id int64) { live(cur) engine.Execute(cur.Previous().Address(), id, func(k governor.Kind, sub, payload string) error { return k.Do(0, cur.Sub(sub), payload) }) } // Cancel withdraws the caller's own open proposal. func Cancel(cur realm, id int64) { live(cur) engine.Cancel(cur.Previous().Address(), id) } // Settle closes a proposal whose voting is over. func Settle(cur realm, id int64) { live(cur) engine.Settle(cur.Previous().Address(), id) } // State, Describe and Tally read a proposal. func State(id int64) string { return engine.State(id) } func DescribeProposal(id int64) string { return engine.Describe(id) } func Tally(id int64) (yes, no, abstain, total int64) { return engine.Tally(id) } // --- the token --- func TotalSupply() int64 { return ledger.TotalSupply() } func BalanceOf(who address) int64 { return ledger.BalanceOf(who) } func VotesOf(who address) int64 { return ledger.VotesOf(who) } func DelegateOf(who address) address { return ledger.DelegateOf(who) } func Allowance(o, s address) int64 { return ledger.Allowance(o, s) } // Transfer sends SUGAR from the caller. func Transfer(cur realm, to address, amount int64) { live(cur) ledger.Transfer(cur.Previous().Address(), to, amount) } // Approve lets spender move up to amount of the caller's SUGAR. func Approve(cur realm, spender address, amount int64) { live(cur) ledger.Approve(cur.Previous().Address(), spender, amount) } // TransferFrom moves SUGAR the owner approved the caller to move. func TransferFrom(cur realm, from, to address, amount int64) { live(cur) ledger.TransferFrom(cur.Previous().Address(), from, to, amount) } // DelegateVotes gives the caller's voting weight to another address (its own, to take it back). func DelegateVotes(cur realm, to address) { live(cur) ledger.Delegate(cur.Previous().Address(), to) }
- #8launch.gno
- #9package sugarproxy import ( "chain" "strconv" "time" ) // The launch: what the first install holds, and a launcher for its first two weeks (Jae, 2026-11-01: "Yes, 14-day // window"). The launcher may add and remove sites, each counting at once, so the exact addresses video sites need // can be found in minutes rather than a vote each; the guardian and GovDAO can remove anything it adds; it can't // touch customers, hubs, payers or releases; and its power ends LaunchDays after the first install, for good. // // owner, launchSeed and launcher are written by deploy/realm/prepare.py just before the realm is deployed, from the // hub's real pin and payer and the first release's hashes. The realm is deployed from a hot key (Jae's Ledger can't // sign a transaction carrying the whole source) under that key's namespace, so the code itself names the owner, // Jae's cold key: it is the first guardian and holds the first SUGAR; the deploying key ends up holding nothing. const LaunchDays = 14 var ( owner address = "g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6" launcher address = "g1w0p203thf56zndjsqv38hn8xm4jynzr7f2nkm9" launchSeed = []string{ "customer|sugar|sugar.video fetches the videos its users paste, to caption, translate and dub them", "hub|hub.sugar.video:8443|da9c7e1b22a7fb1c9af7aed7e4268691625718260ebeb57192d671c3758a04f8", "payer|g14qh9d2y2wrzwfhzneu8d4wx0fem93yq76zjsux", "release|0.1.0|darwin-amd64|255f74db2f00164f3236d7004a9628a1c1afa97bef44ee1a56e36c209e1056a5|https://sugar.video/proxy/dl/sugar-proxy-0.1.0-darwin-amd64", "release|0.1.0|darwin-arm64|d363ec51db2e869d561715bb7f5df8c0103fa035f8fd0f05f8e712df2d955332|https://sugar.video/proxy/dl/sugar-proxy-0.1.0-darwin-arm64", "release|0.1.0|linux-amd64|0db436c9ea1557d9560dc03e565b930f5539b79409a50e9cd80af99d68e57f39|https://sugar.video/proxy/dl/sugar-proxy-0.1.0-linux-amd64", "release|0.1.0|linux-arm64|98d784373569d758b9f0eee7acd3d6463e63ede1229770cbe3f9bcba886fde28|https://sugar.video/proxy/dl/sugar-proxy-0.1.0-linux-arm64", "release|0.1.0|windows-amd64|8e57e22bbc8951b2f86990a340f03567d330b48bb6807b636f3dce26207fb117|https://sugar.video/proxy/dl/sugar-proxy-0.1.0-windows-amd64.exe", "site|youtube.com|sugar|YouTube pages of the videos sugar.video users paste", "site|www.youtube.com|sugar|YouTube pages of the videos sugar.video users paste", "site|m.youtube.com|sugar|YouTube pages of the videos sugar.video users paste", "site|youtu.be|sugar|YouTube short links sugar.video users paste", "site|*.googlevideo.com|sugar|the video files of those YouTube videos", "site|vimeo.com|sugar|Vimeo pages of the videos sugar.video users paste", "site|player.vimeo.com|sugar|Vimeo's player for those videos", "site|*.vimeocdn.com|sugar|the video files of those Vimeo videos", "site|www.reddit.com|sugar|Reddit posts with the videos sugar.video users paste", "site|v.redd.it|sugar|the video files of those Reddit posts", } launchEnd int64 ) // launch seeds the first install's items, counting at once (they are in the deployed code, which Jae signs), and // starts the launcher's window. init calls it. func launch() { launchEnd = time.Now().Unix() + LaunchDays*86400 launching = true // the doc is written once, after the last item, not after each for _, p := range launchSeed { if _, err := add(p, "init"); err != nil { panic("launch item " + p + ": " + err.Error()) } } launching = false publish() } // Launcher is the launcher's address and when its window ends (unix seconds). func Launcher() (address, int64) { return launcher, launchEnd } // LauncherAdd adds a site ("site|pattern|customer|purpose"), counting at once, from the launcher during its window. func LauncherAdd(cur realm, payload string) int { mustLauncher(cur) kind, _, err := parse(payload) if err != nil { panic(err.Error()) } if kind != KindSite { panic("the launcher adds sites only") } id, err := add(payload, "launcher") if err != nil { panic(err.Error()) } return id } // LauncherRemove removes a site, from the launcher during its window. func LauncherRemove(cur realm, id int) { mustLauncher(cur) if it := find(id); it == nil || it.Kind != KindSite { panic("the launcher removes sites only") } if err := end(id, false, "launcher"); err != nil { panic(err.Error()) } } func mustLauncher(cur realm) { if launcher == "" || mustUser(cur) != launcher { panic("only the launcher may do that") } if time.Now().Unix() >= launchEnd { panic("the launch window ended at " + strconv.FormatInt(launchEnd, 10)) } chain.Emit("Launcher", "by", launcher.String()) }
- #10pay.gno
- #11package sugarproxy import ( "chain" "chain/banker" "chain/runtime/unsafe" "math/overflow" "strconv" "strings" "time" "gno.land/p/nt/avl/v0" ) // Paying sharers. The hub tallies the bytes each sharer's app carried and pays them through Pay, so every payout // is on chain: who was paid, how much, for how many bytes, in which period. The hub's account is an approved payer; // it signs with a gno.land session key whose spending the chain caps (only calls to this realm, so much a period), // so a stolen hub key can pay out at most one period's worth, and only through here, where it shows. const denom = "ugnot" // Totals is what a sharer has been paid, all told. type Totals struct { Paid int64 // ugnot Bytes int64 Payouts int Last int64 // unix seconds } // Payout is one call to Pay. type Payout struct { Time int64 Payer address Period string Receipts string // the hex sha256 of the payer's receipts for the period: the confirmed downloads it paid for Count int Ugnot int64 Bytes int64 } var ( totals = avl.NewTree() // address -> *Totals payouts []*Payout ) // Pay pays sharers with the GNOT sent along. lines has one sharer a line: "address ugnot bytes"; the amounts must // add up to what was sent, and a payout is at most 500 lines. period names what it pays for ("2026-11-01", say); // receipts is the hex sha256 of the payer's receipts for it (one line per confirmed download: its tag, the file's // sha256 and size, who carried how much), recorded here so the receipts can be checked against it later. Only an // approved payer may call it; pausing doesn't stop it (sharers are owed for what they carried). func Pay(cur realm, period, receipts, lines string) { payer := mustUser(cur) if !payerActive(payer) { panic("only an approved payer may pay") } if len(period) == 0 || len(period) > 40 || strings.ContainsAny(period, "\n\r\t") { panic("the period is 1 to 40 characters on one line") } if !hexOf(receipts, 64) { panic("receipts is the receipts' sha256: 64 lowercase hex") } sent := coinsSent(cur) type line struct { to address ugnot, bytes int64 } var all []line var sum, bytes int64 for _, l := range strings.Split(strings.TrimSpace(lines), "\n") { f := strings.Fields(l) if len(f) != 3 { panic("each line is: address ugnot bytes") } to := address(f[0]) u, err1 := strconv.ParseInt(f[1], 10, 64) b, err2 := strconv.ParseInt(f[2], 10, 64) if !to.IsValid() || err1 != nil || err2 != nil || u <= 0 || b < 0 { panic("a bad line: " + l) } sum = overflow.Add64p(sum, u) bytes = overflow.Add64p(bytes, b) all = append(all, line{to, u, b}) } if len(all) == 0 || len(all) > 500 { panic("a payout is 1 to 500 lines") } if sum != sent { panic("the amounts add up to " + strconv.FormatInt(sum, 10) + " ugnot, but " + strconv.FormatInt(sent, 10) + " were sent") } now := time.Now().Unix() bk := banker.NewBanker(banker.BankerTypeRealmSend, cur) for _, l := range all { bk.SendCoins(cur.Address(), l.to, chain.Coins{chain.NewCoin(denom, l.ugnot)}) t, _ := totals.Get(l.to.String()).(*Totals) if t == nil { t = &Totals{} totals.Set(l.to.String(), t) } t.Paid = overflow.Add64p(t.Paid, l.ugnot) t.Bytes = overflow.Add64p(t.Bytes, l.bytes) t.Payouts++ t.Last = now chain.Emit("Paid", "to", l.to.String(), "ugnot", strconv.FormatInt(l.ugnot, 10), "bytes", strconv.FormatInt(l.bytes, 10), "period", period) } payouts = append(payouts, &Payout{Time: now, Payer: payer, Period: period, Receipts: receipts, Count: len(all), Ugnot: sum, Bytes: bytes}) chain.Emit("Payout", "period", period, "receipts", receipts, "sharers", strconv.Itoa(len(all)), "ugnot", strconv.FormatInt(sum, 10)) } // PaidTo is what a sharer has been paid, all told. func PaidTo(a address) (ugnot, bytes int64, count int) { if t, ok := totals.Get(a.String()).(*Totals); ok && t != nil { return t.Paid, t.Bytes, t.Payouts } return 0, 0, 0 } func payerActive(a address) bool { now := time.Now().Unix() for _, it := range items { if it.Kind == KindPayer && it.Fields[0] == a.String() && it.State == StateApproved && now >= it.After { return true } } return false } // coinsSent returns the ugnot sent with a direct call, refusing other denoms. Coins sent with a run go to the run's // own address, not here, so a run sends none. func coinsSent(cur realm) int64 { if !cur.Previous().IsUserCall() { return 0 } var n int64 for _, c := range unsafe.OriginSend() { if c.Amount == 0 { continue } if c.Denom != denom || c.Amount < 0 { panic("only " + denom + " is taken") } n = overflow.Add64p(n, c.Amount) } return n }
- #12render.gno
- #13package sugarproxy import ( "strconv" "strings" "time" ) // Render shows the rules as people read them on gno.land. func Render(path string) string { now := time.Now().Unix() var b strings.Builder b.WriteString("# sugar proxy\n\nHome internet connections, shared on purpose and paid in GNOT. Sharers run the sugar proxy app; " + "it carries only what these rules allow, checked against this realm through a light client. See sugar.video/proxy.\n\n") if paused { b.WriteString("**Paused.** Apps carry nothing until GovDAO or the guardian resumes it.\n\n") } sections := []struct{ kind, title string }{ {KindSite, "Sites"}, {KindCustomer, "Customers"}, {KindRelease, "App releases"}, {KindHub, "Hubs"}, {KindPayer, "Payers"}, } for _, s := range sections { b.WriteString("## " + s.title + "\n\n") n := 0 for _, it := range items { if it.Kind != s.kind { continue } n++ b.WriteString("- **" + strconv.Itoa(it.ID) + "** `" + joinFields(it.Fields) + "`: ") switch { case it.State != StateApproved: b.WriteString(it.Note) case now < it.After: b.WriteString("approved by " + it.By + "; counts from " + time.Unix(it.After, 0).UTC().Format(time.RFC3339) + " unless vetoed") default: b.WriteString("counts (approved by " + it.By + ")") } b.WriteString("\n") } if n == 0 { b.WriteString("None yet.\n") } b.WriteString("\n") } b.WriteString("## Who decides\n\n") b.WriteString("- SUGAR holders, by the governor: propose `" + kindAdd + "` with `kind|field|...`, or `" + kindRemove + "` with an item's number.\n") if delegate != "" { b.WriteString("- The delegate realm `" + delegate + "` adds and removes sites.\n") } else { b.WriteString("- No delegate realm is named.\n") } b.WriteString("- GovDAO adds and removes anything at once, vetoes, pauses, and names the delegate and the guardian.\n") if guardian != "" { b.WriteString("- The guardian, " + guardian.String() + ", may veto and pause.\n") } b.WriteString("- Veto windows: sites " + days(vetoWin[KindSite]) + ", releases " + days(vetoWin[KindRelease]) + ", hubs, customers and payers " + days(vetoWin[KindHub]) + ".\n\n") open := 0 for _, r := range requests { if !r.Done { if open == 0 { b.WriteString("## Suggestions\n\n") } open++ b.WriteString("- " + strconv.Itoa(r.ID) + " `" + r.Payload + "` by " + r.By.String() + ": " + r.Reason + "\n") } } if open > 0 { b.WriteString("\n") } if len(payouts) > 0 { var paid int64 for _, p := range payouts { paid += p.Ugnot } b.WriteString("## Payouts\n\n" + strconv.Itoa(len(payouts)) + " payouts, " + strconv.FormatInt(paid/1_000_000, 10) + " GNOT in all.\n\n") } b.WriteString("The rules apps read: params `doc`, sha256 " + digest + ", version " + strconv.Itoa(docSeq) + ".\n") return b.String() } func days(s int64) string { if s%86400 == 0 { return strconv.FormatInt(s/86400, 10) + " days" } return strconv.FormatInt(s/3600, 10) + " hours" }
- #14sugarproxy.gno
- #15// Package sugarproxy holds the rules of sugar proxy (sugar repo, PROXY.md): which sites sharers' home connections // may carry, for which customers and why; which app releases they may run; which hubs they may connect to; who pays // them. Sharers' apps read these rules through a light client, from one parameter this realm writes on every change // (params "doc": the canonical JSON of everything below), so no hub can change what a sharer's connection does. // // Who decides: // - Holders of the SUGAR token, through kourt's governor (govern.gno): they add and remove anything. // - A delegate realm, if they name one (gno.security's, say): it adds and removes sites, nothing else. // - GovDAO (govdao.gno), above both: it adds and removes anything at once, vetoes, pauses, and names the delegate // and the guardian. A guardian it names (the deployer, at first) may veto and pause, never approve. // // Anything added by the token or the delegate waits out a veto window before it counts; GovDAO's own additions // count at once. A veto or a removal counts at once, whoever makes it. package sugarproxy import ( "chain" "chain/params" "crypto/sha256" "encoding/hex" "strconv" "strings" "time" "gno.land/p/nt/ufmt/v0" ) func errorf(format string, args ...any) error { return ufmt.Errorf(format, args...) } // Item is one entry of the rules: a site, a release, a hub, a customer or a payer. type Item struct { ID int Kind string // KindSite, KindRelease, KindHub, KindCustomer, KindPayer Fields []string State string // StateApproved, StateVetoed, StateRevoked By string // who added it: "init", "token", "delegate", "govdao" Approved int64 // unix seconds After int64 // unix seconds: when its veto window ends and it counts Note string // who vetoed or removed it, and when } const ( KindSite = "site" // fields: pattern, customer, purpose KindRelease = "release" // fields: version, platform, sha256, url KindHub = "hub" // fields: host:port, pin (hex sha256 of its certificate's public key) KindCustomer = "customer" // fields: name, purpose KindPayer = "payer" // fields: address (may call Pay) StateApproved = "approved" StateVetoed = "vetoed" StateRevoked = "revoked" ) var ( items []*Item nextID = 1 paused bool delegate string // the realm path that may add and remove sites; "" is none guardian address // may veto and pause; GovDAO names it vetoWin = map[string]int64{KindSite: 3 * 86400, KindRelease: 7 * 86400, KindHub: 3 * 86400, KindCustomer: 3 * 86400, KindPayer: 3 * 86400} doc string // the canonical JSON the apps read, as params "doc" digest string // its hex sha256 docSeq int // how many times it has changed requests []*Request nextReqID = 1 launching bool // launch.gno: while the first install's items go in ) // Request is anyone's suggestion of an item, for the token holders or the delegate to consider. It changes nothing. type Request struct { ID int By address Payload string Reason string Time int64 Done bool } func init(cur realm) { setupGovernor() // The owner (launch.gno; the deployer when unset) is the first guardian (GovDAO can replace it) and holds the // whole first supply of SUGAR (govern.gno). who := owner if who == "" { who = cur.Previous().Address() } if who != "" { guardian = who mintInitial(who) } launch() publish() } // --- what an item may say --- // parse turns "kind|field|field..." into an item's kind and fields, checking each field. func parse(payload string) (string, []string, error) { parts := strings.Split(payload, "|") kind, f := parts[0], parts[1:] want := map[string]int{KindSite: 3, KindRelease: 4, KindHub: 2, KindCustomer: 2, KindPayer: 1}[kind] if want == 0 { return "", nil, errorf("unknown kind %q: site, release, hub, customer or payer", kind) } if len(f) != want { return "", nil, errorf("a %s has %d fields, separated by |", kind, want) } for _, x := range f { if len(x) == 0 || len(x) > 300 || strings.ContainsAny(x, "\n\r\t") { return "", nil, errorf("each field is 1 to 300 characters on one line") } } switch kind { case KindSite: if !sitePattern(f[0]) { return "", nil, errorf("%q is not a site: a lowercase DNS name, or *. and one", f[0]) } if !customerActive(f[1]) { return "", nil, errorf("no customer %q is approved", f[1]) } case KindRelease: if !word(f[0], 1, 32) || !word(f[1], 3, 32) || !hexOf(f[2], 64) || !strings.HasPrefix(f[3], "https://") { return "", nil, errorf("a release is version|platform|sha256 (64 hex)|https://url") } case KindHub: host, port, ok := strings.Cut(f[0], ":") n, err := strconv.Atoi(port) if !ok || !sitePattern(host) || strings.HasPrefix(host, "*.") || err != nil || n < 1 || n > 65535 || !hexOf(f[1], 64) { return "", nil, errorf("a hub is host:port|pin (64 hex)") } case KindCustomer: if !word(f[0], 2, 32) { return "", nil, errorf("a customer's name is 2 to 32 of a-z, 0-9 and -") } case KindPayer: if !address(f[0]).IsValid() { return "", nil, errorf("%q is not an address", f[0]) } } return kind, f, nil } func word(s string, min, max int) bool { if len(s) < min || len(s) > max { return false } for i := 0; i < len(s); i++ { c := s[i] if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-' || c == '.' || c == '_') { return false } } return true } func hexOf(s string, n int) bool { if len(s) != n { return false } _, err := hex.DecodeString(s) return err == nil && strings.ToLower(s) == s } // sitePattern is a lowercase DNS name of two or more labels, or "*." and one: what an app matches a host against. func sitePattern(s string) bool { s = strings.TrimPrefix(s, "*.") if len(s) > 253 { return false } labels := strings.Split(s, ".") if len(labels) < 2 { return false } for _, l := range labels { if len(l) == 0 || len(l) > 63 || l[0] == '-' || l[len(l)-1] == '-' { return false } for i := 0; i < len(l); i++ { c := l[i] if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-') { return false } } } return strings.Trim(labels[len(labels)-1], "0123456789") != "" } func customerActive(name string) bool { now := time.Now().Unix() for _, it := range items { if it.Kind == KindCustomer && it.Fields[0] == name && it.State == StateApproved && now >= it.After { return true } } return false } // --- changes: every one goes through here and republishes the doc --- // add puts an item in, approved. by says who: its veto window applies unless GovDAO added it. func add(payload, by string) (int, error) { if paused && by != "govdao" { return 0, errorf("sugar proxy is paused") } kind, fields, err := parse(payload) if err != nil { return 0, err } if by == "delegate" && kind != KindSite { return 0, errorf("the delegate adds sites only") } for _, it := range items { if it.Kind == kind && strings.Join(it.Fields, "|") == strings.Join(fields, "|") && it.State == StateApproved { return 0, errorf("item %d already says that", it.ID) } } now := time.Now().Unix() after := now + vetoWin[kind] if by == "govdao" || by == "init" || by == "launcher" { after = now // GovDAO is the veto; the first install is signed by Jae; the launcher's sites are removable at once } it := &Item{ID: nextID, Kind: kind, Fields: fields, State: StateApproved, By: by, Approved: now, After: after} nextID++ items = append(items, it) for _, r := range requests { if !r.Done && r.Payload == payload { r.Done = true } } chain.Emit("Added", "id", strconv.Itoa(it.ID), "kind", kind, "by", by, "after", strconv.FormatInt(after, 10)) publish() return it.ID, nil } // end vetoes or removes an item: a veto only while its window is open, a removal any time. It counts at once. func end(id int, veto bool, by string) error { it := find(id) if it == nil { return errorf("no item %d", id) } if it.State != StateApproved { return errorf("item %d is already %s", id, it.State) } now := time.Now().Unix() if veto { if now >= it.After { return errorf("item %d's veto window is over; remove it instead", id) } it.State = StateVetoed } else { it.State = StateRevoked } it.Note = it.State + " by " + by + " at " + strconv.FormatInt(now, 10) chain.Emit("Ended", "id", strconv.Itoa(id), "state", it.State, "by", by) publish() return nil } func setPaused(on bool, by string) { if paused == on { return } paused = on chain.Emit("Paused", "on", strconv.FormatBool(on), "by", by) publish() } func find(id int) *Item { for _, it := range items { if it.ID == id { return it } } return nil } // publish writes the doc the apps read: params "doc", at gno.land's main store key // "/pv/vm:<this realm's path>:doc", which a light client proves against a block's app hash. func publish() { if launching { return } doc = render() sum := sha256.Sum256([]byte(doc)) digest = hex.EncodeToString(sum[:]) docSeq++ params.SetBytes("doc", []byte(doc)) } // render is the canonical JSON of the rules: every item ever approved, in order, with its state, so an app can // tell what counts at any time (approved, and its After passed) without trusting anyone's clock but its own. func render() string { var b strings.Builder b.WriteString(`{"v":1,"seq":`) b.WriteString(strconv.Itoa(docSeq + 1)) b.WriteString(`,"paused":`) b.WriteString(strconv.FormatBool(paused)) b.WriteString(`,"items":[`) for i, it := range items { if i > 0 { b.WriteByte(',') } b.WriteString(`{"id":`) b.WriteString(strconv.Itoa(it.ID)) b.WriteString(`,"kind":`) b.WriteString(jstr(it.Kind)) b.WriteString(`,"fields":[`) for j, f := range it.Fields { if j > 0 { b.WriteByte(',') } b.WriteString(jstr(f)) } b.WriteString(`],"state":`) b.WriteString(jstr(it.State)) b.WriteString(`,"by":`) b.WriteString(jstr(it.By)) b.WriteString(`,"approved":`) b.WriteString(strconv.FormatInt(it.Approved, 10)) b.WriteString(`,"after":`) b.WriteString(strconv.FormatInt(it.After, 10)) b.WriteByte('}') } b.WriteString(`]}`) return b.String() } // jstr quotes s as a JSON string. func jstr(s string) string { const hexd = "0123456789abcdef" var b strings.Builder b.WriteByte('"') for i := 0; i < len(s); i++ { c := s[i] switch { case c == '"': b.WriteString(`\"`) case c == '\\': b.WriteString(`\\`) case c < 0x20: b.WriteString(`\u00`) b.WriteByte(hexd[c>>4]) b.WriteByte(hexd[c&15]) default: b.WriteByte(c) } } b.WriteByte('"') return b.String() } // --- reading --- // Doc is the canonical JSON the apps read (the same bytes as params "doc"); Digest is its hex sha256. func Doc() string { return doc } func Digest() string { return digest } // ItemID is the number of the approved item that says exactly payload ("kind|field|..."), or 0. func ItemID(payload string) int { for _, it := range items { if it.State == StateApproved && it.Kind+"|"+joinFields(it.Fields) == payload { return it.ID } } return 0 } // Paused reports whether sugar proxy is paused: apps carry nothing while it is. func Paused() bool { return paused } // Delegate is the realm that may add and remove sites ("" is none); Guardian may veto and pause. func Delegate() string { return delegate } func Guardian() address { return guardian } // VetoWindow is how long an added item of a kind waits before it counts, in seconds. func VetoWindow(kind string) int64 { return vetoWin[kind] } // --- anyone --- // Suggest records a suggestion of an item ("kind|field|..."), for the token holders or the delegate to consider. It // changes nothing. Each address may have three open at a time. func Suggest(cur realm, payload, reason string) int { by := mustUser(cur) if _, _, err := parse(payload); err != nil { panic(err.Error()) } if len(reason) > 500 { panic("the reason is at most 500 characters") } open := 0 for _, r := range requests { if r.By == by && !r.Done { open++ } } if open >= 3 { panic("you have three suggestions open already") } r := &Request{ID: nextReqID, By: by, Payload: payload, Reason: reason, Time: time.Now().Unix()} nextReqID++ requests = append(requests, r) chain.Emit("Suggested", "id", strconv.Itoa(r.ID), "by", by.String()) return r.ID } // Withdraw closes your own suggestion. func Withdraw(cur realm, id int) { by := mustUser(cur) for _, r := range requests { if r.ID == id && r.By == by { r.Done = true return } } panic("no suggestion of yours has that id") } // --- the delegate realm --- // DelegateAdd adds a site ("site|pattern|customer|purpose"), from the delegate realm only; it waits out the // site veto window. func DelegateAdd(cur realm, payload string) int { mustDelegate(cur) id, err := add(payload, "delegate") if err != nil { panic(err.Error()) } return id } // DelegateRemove removes a site, from the delegate realm only. func DelegateRemove(cur realm, id int) { mustDelegate(cur) if it := find(id); it == nil || it.Kind != KindSite { panic("the delegate removes sites only") } if err := end(id, false, "delegate"); err != nil { panic(err.Error()) } } // mustDelegate refuses any caller but the delegate realm itself, the way r/sys/params checks its delegates: none // named refuses everyone, and the caller's realm path must match exactly. func mustDelegate(cur realm) { if delegate == "" { panic("no delegate is named") } if cur.Previous().PkgPath() != delegate { panic("only the delegate realm " + delegate + " may do that") } } // --- the guardian --- // GuardianVeto vetoes an item still in its veto window. func GuardianVeto(cur realm, id int) { mustGuardian(cur) if err := end(id, true, "guardian"); err != nil { panic(err.Error()) } } // GuardianRemove removes a site, at once: the safe direction, so the guardian may (the launcher's sites count at // once, so there is no window to veto them in). func GuardianRemove(cur realm, id int) { mustGuardian(cur) if it := find(id); it == nil || it.Kind != KindSite { panic("the guardian removes sites only") } if err := end(id, false, "guardian"); err != nil { panic(err.Error()) } } // GuardianPause pauses or resumes sugar proxy. func GuardianPause(cur realm, on bool) { mustGuardian(cur) setPaused(on, "guardian") } func mustGuardian(cur realm) { if guardian == "" || mustUser(cur) != guardian { panic("only the guardian may do that") } } // mustUser returns the caller, who must be a user, not another realm's code. func mustUser(cur realm) address { prev := cur.Previous() if !prev.IsUser() { panic("only a user may call this") } return prev.Address() }
- #16/gno.MemPackageType
- #17 MPUserAll
Result log
msg:0,success:true,log:,events:[]