Transaction
9CD0DBC13D9B20…1157E36D4DD9
Block 424,887 · index 0 · indexed
Summary
- Hash
- 9CD0DBC13D9B2011AF839755B7F33EB8C432404975F48A093D371157E36D4DD9
- Block
- 424,887
- Size
- 21364 bytes
- Gas used
- 27,373,062 / 101,469,000
- Fee
- 304407ugnot
- Status
- success
Messages
- Attached funds
- 5000000ugnot
- Package
- gno.land/r/moul/pilot/v0
Arguments · 9
- #1pilot
- #2README.md
- #3# `gno.land/r/moul/pilot/v0` moul's realm-driven account. It holds the funds and the identity; moul's key pilots it; its powers arrive afterwards as separate realms this one never imports. All behaviour is in [`p/moul/pilot`](../../../p/moul/pilot), which explains the two grants and why `Revoke` takes back a purse but never an identity. This realm is the instance: a `*pilot.Pilot`, one-line crossing re-exports forwarding `cur`, and `Render`. A power to try it with: [`r/moul/x/pilotdemo`](../x/pilotdemo). **`Claim` is pinned to `g1manfred47kzduec920z88wfr64ylksmdcedlf5` in the source.** A deploy lands in one block and the claim is a second transaction, so taking the owner from whoever calls first is a race anyone on chain can win, and this path can never be redeployed to undo it. ```sh # once, and only this address can gnokey maketx call -pkgpath gno.land/r/moul/pilot/v0 -func Claim ... moul # authorise a path that does not have to exist yet gnokey maketx call -pkgpath gno.land/r/moul/pilot/v0 -func Approve \ -args gno.land/r/moul/x/pilotdemo/v0 -args payout -args false -args 1000 ... moul ``` **Public on purpose.** A module realm imports this one and persists objects it owns, both of which a private realm refuses, and a redeploy would wipe the owner, the roster and every budget while leaving the coins at the treasury address. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:**  > ⚠️ **Disclaimer:** provided as-is, without warranty; not security-audited. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
- #4gnomod.toml
- #5module = "gno.land/r/moul/pilot/v0" gno = "0.9" # public: a module realm must import this account and persist objects it owns # (a Purse, the Account handle), both of which a private realm refuses. And a # redeploy wipes every package-level var while keeping the coins, which for an # account means the owner, the roster and every budget vanish while the # treasury stays: the one realm that must never be replaceable.
- #6pilot.gno
- #7// untrusted-render: this realm stores no string of its own. The only two a // caller supplies, a module path and its subpath, are written solely by the // owner through Approve and charset-validated there (p/moul/pilot assertPlain), // which is what lets Render interpolate them. // Package pilot is moul's realm-driven account: it holds the funds and the // identity, moul's key pilots it, and its powers arrive afterwards as // separate realms that this one never imports. // // All behaviour is in gno.land/p/moul/pilot/v0; this realm is the instance. // Demo of a power: r/moul/x/pilotdemo. package pilot import "gno.land/p/moul/pilot/v0" // owner is pinned in the source rather than taken from whoever calls Claim // first. A deploy lands in its own block and the claim is a second // transaction, so an unpinned Claim is a race anyone on chain can win, and // this account can never be redeployed to undo it. const owner = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" var acct *pilot.Pilot // Claim arms the account. Once, and only by its owner. func Claim(cur realm) { if acct != nil { panic("pilot: already claimed") } if cur.Previous().Address() != owner { panic("pilot: only " + owner + " can claim this account") } acct = pilot.New(0, cur) } // Approve authorises a package path to install itself later, under a named // sub-identity, with a grant and a budget. The code need not exist yet. func Approve(cur realm, path, subpath string, identity bool, budget int64) { grant := pilot.GrantPurse if identity { grant = pilot.GrantIdentity } must().Approve(0, cur, path, subpath, grant, budget) } // SetBudget changes what a module may still spend, including through a purse // it already holds. func SetBudget(cur realm, path string, budget int64) { must().SetBudget(0, cur, path, budget) } // Revoke stops a module. It cannot take back a granted identity. func Revoke(cur realm, path string) { must().Revoke(0, cur, path) } // Fund moves coins from the main treasury into one module's sub-treasury. func Fund(cur realm, path string, amount int64) { must().Fund(0, cur, path, amount) } // Exec drives an installed module. func Exec(cur realm, path, args string) string { return must().Exec(0, cur, path, args) } // Handle is how a module realm reaches this account. Its two methods key on // the caller's own pkgpath, which no realm can forge for another. func Handle() *pilot.Account { return must().Handle() } // Address is the main treasury. func Address() address { return must().Address() } // SubAddress is one module's treasury. func SubAddress(path string) address { return must().SubAddress(path) } func Render(path string) string { if acct == nil { return "# gno.land/r/moul/pilot\n\nUnclaimed.\n" } return acct.Render(path) } func must() *pilot.Pilot { if acct == nil { panic("pilot: unclaimed") } return acct }
- #8pilot_test.gno
- #9package pilot import ( "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) var ( // the address Claim is pinned to, not a test address moul = address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") someone = testutils.TestAddress("someone") ) func TestClaimIsOnceAndOwnerGated(cur realm, t *testing.T) { // anyone else racing the deploy is refused, which is the whole point of // pinning the address in the source testing.SetRealm(testing.NewUserRealm(someone)) uassert.AbortsContains(t, cur, "only g1manfred", func() { Claim(cross(cur)) }) testing.SetRealm(testing.NewUserRealm(moul)) Claim(cross(cur)) uassert.Equal(t, moul.String(), acct.Owner().String()) testing.SetRealm(testing.NewUserRealm(someone)) uassert.AbortsContains(t, cur, "already claimed", func() { Claim(cross(cur)) }) uassert.AbortsContains(t, cur, "not the owner", func() { Approve(cross(cur), "gno.land/r/x/nope/v0", "nope", false, 1) }) testing.SetRealm(testing.NewUserRealm(moul)) uassert.AbortsContains(t, cur, "[a-zA-Z0-9._/-]", func() { Approve(cross(cur), "gno.land/r/x/nope/v0", "pay|out", false, 1) }) } func TestRender(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(moul)) Approve(cross(cur), "gno.land/r/moul/x/pilotdemo/v0", "payout", false, 250) want := "# gno.land/r/moul/pilot/v0\n\n" + "| | |\n|---|---|\n" + "| owner | " + moul.String() + " |\n" + "| treasury | " + Address().String() + " |\n" + "| balance | 0 ugnot |\n\n" + "## powers\n\n" + "| path | grant | identity | state | left | runs | sub-treasury |\n" + "|---|---|---|---|---|---|---|\n" + "| `gno.land/r/moul/x/pilotdemo/v0` | purse | `gno.land/r/moul/pilot/v0#payout` | approved | 250 | 0 | 0 ugnot |\n" uassert.Equal(t, want, Render("")) }
- Attached funds
- 5000000ugnot
Arguments · 7
- #1bad
- #2README.md
- #3# `gno.land/r/moul/x/upgrade/schema/impl/bad/v0` The handler that **exists to be refused**. It satisfies `facade.Handler` exactly as well as the others do, and drops a verb and reshapes another, which only a schema diff can see. Nothing in gnos type system can. See [the pattern](../../README.md) and [the exploration](../../../README.md). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:**  > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
- #4bad.gno
- #5// Package bad is the handler realm that exists to be refused (pattern G; see // ../../README.md). // // It satisfies facade.Handler exactly as well as the others do: same methods, // same signatures, compiles and proposes itself without complaint. What it does // is drop the "upper" verb and change "repeat" to take one argument instead of // two, both of which break a caller already compiled against v0. // // Nothing in gno's type system can see that. A Go interface says what methods // exist, not which verbs the data behind them still answers, which is the whole // argument for declaring the API as data and diffing it at accept time. package bad import ( facade "gno.land/r/moul/x/upgrade/schema/facade/v0" ) // schema drops "upper" entirely and gives "repeat" the wrong arity. const schema = `repeat s` type handler struct{} func (handler) Schema() string { return schema } func (handler) Invoke(verb string, args []string) string { return "this handler never gets to run" } // Instance exposes the singleton. func Instance() facade.Handler { return handler{} } // Path is this realm's own package path. const Path = "gno.land/r/moul/x/upgrade/schema/impl/bad/v0" func init(cur realm) { facade.Propose(cross(cur), Instance()) }
- #6gnomod.toml
- #7module = "gno.land/r/moul/x/upgrade/schema/impl/bad/v0" gno = "0.9" # public: hands its own handler to the facade; private aborts with "cannot persist object from the private realm"
- Attached funds
- 5000000ugnot
Arguments · 7
- #1impl
- #2README.md
- #3# `gno.land/r/moul/x/upgrade/schema/impl/v0` Version 0 of the **handler realm** of pattern G. Two verbs, `upper` and `repeat`, declared as text rather than as a method set. See [the pattern](../../README.md) and [the exploration](../../../README.md). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:**  > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
- #4gnomod.toml
- #5module = "gno.land/r/moul/x/upgrade/schema/impl/v0" gno = "0.9" # public: hands its own handler to the facade; private aborts with "cannot persist object from the private realm"
- #6impl.gno
- #7// Package impl is version 0 of the handler realm of the "API as data" upgrade // pattern (pattern G; see ../../README.md). // // Two verbs, declared as text rather than as a method set. Deploying nominates // it; the facade owner accepts it by path. package impl import ( "strings" facade "gno.land/r/moul/x/upgrade/schema/facade/v0" ) // schema is this version's whole API. One verb per line, name then parameters. const schema = `upper s repeat s n` type handler struct{} func (handler) Schema() string { return schema } func (handler) Invoke(verb string, args []string) string { switch verb { case "upper": return strings.ToUpper(args[0]) case "repeat": return strings.Repeat(args[0], atoi(args[1])) } // Unreachable: the facade checked the verb against this schema first. panic("unknown verb " + verb) } // atoi is hand-rolled to keep the failure message about the ARGUMENT rather // than about strconv, which is the whole difference a typed boundary would have // made here. func atoi(s string) int { if s == "" { panic("expected a number, got an empty argument") } n := 0 for _, c := range s { if c < '0' || c > '9' { panic("expected a number, got " + s) } n = n*10 + int(c-'0') if n > 64 { panic("refusing to repeat more than 64 times") } } return n } // Instance exposes the singleton. func Instance() facade.Handler { return handler{} } // Path is this realm's own package path, the string the facade owner accepts. const Path = "gno.land/r/moul/x/upgrade/schema/impl/v0" func init(cur realm) { facade.Propose(cross(cur), Instance()) } // Propose re-nominates this handler, so a rollback needs no redeploy. func Propose(cur realm) { facade.Propose(cross(cur), Instance()) }
- Attached funds
- 5000000ugnot
Arguments · 9
- #1impl
- #2README.md
- #3# `gno.land/r/moul/x/upgrade/schema/impl/v1` Version 1 of the **handler realm** of pattern G. Adds `surround` and keeps both of [`v0`](../gen0)s verbs untouched, which is what lets the facade accept it: growing the API costs no new realm and no new signature at the permanent path. Also carries the twin of `adminreg`s dispatch-cost test. See [the pattern](../../README.md) and [the exploration](../../../README.md). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:**  > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
- #4gnomod.toml
- #5module = "gno.land/r/moul/x/upgrade/schema/impl/v1" gno = "0.9" # public: hands its own handler to the facade; private aborts with "cannot persist object from the private realm"
- #6impl.gno
- #7// Package impl is version 1 of the handler realm of the "API as data" upgrade // pattern (pattern G; see ../../README.md). // // Three verbs: it ADDS one and keeps both of v0's untouched, which is what // lets the facade accept it. Growing the API costs no new realm and no new // signature at the permanent path. package impl import ( "strings" facade "gno.land/r/moul/x/upgrade/schema/facade/v0" ) // schema is this version's whole API. One verb per line, name then parameters. const schema = `upper s repeat s n surround s left right` type handler struct{} func (handler) Schema() string { return schema } func (handler) Invoke(verb string, args []string) string { switch verb { case "upper": return strings.ToUpper(args[0]) case "repeat": return strings.Repeat(args[0], atoi(args[1])) case "surround": return args[1] + args[0] + args[2] } // Unreachable: the facade checked the verb against this schema first. panic("unknown verb " + verb) } // atoi is hand-rolled to keep the failure message about the ARGUMENT rather // than about strconv, which is the whole difference a typed boundary would have // made here. func atoi(s string) int { if s == "" { panic("expected a number, got an empty argument") } n := 0 for _, c := range s { if c < '0' || c > '9' { panic("expected a number, got " + s) } n = n*10 + int(c-'0') if n > 64 { panic("refusing to repeat more than 64 times") } } return n } // Instance exposes the singleton. func Instance() facade.Handler { return handler{} } // Path is this realm's own package path, the string the facade owner accepts. const Path = "gno.land/r/moul/x/upgrade/schema/impl/v1" func init(cur realm) { facade.Propose(cross(cur), Instance()) } // Propose re-nominates this handler, so a rollback needs no redeploy. func Propose(cur realm) { facade.Propose(cross(cur), Instance()) }
- #8impl_test.gno
- #9package impl import ( "testing" facade "gno.land/r/moul/x/upgrade/schema/facade/v0" bad "gno.land/r/moul/x/upgrade/schema/impl/bad/v0" implv0 "gno.land/r/moul/x/upgrade/schema/impl/v0" "gno.land/p/nt/uassert/v0" ) // TestGrowTheAPI is the whole pattern: three handlers propose themselves, the // API is enumerable without a transaction, it grows without a new signature at // the permanent path, and the one candidate that would break a caller is // refused despite satisfying the interface. func TestGrowTheAPI(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(facade.Ownable.Owner())) uassert.Equal(t, 3, len(facade.Candidates()), "all three handlers nominated themselves") // v0: two verbs, and a caller can discover them with no transaction. facade.Accept(cross(cur), implv0.Path) uassert.Equal(t, "upper, repeat", joinVerbs(facade.Verbs())) uassert.Equal(t, "repeat(s, n)", facade.Signature("repeat")) uassert.Equal(t, "GNO", facade.Call(cross(cur), "upper", "gno")) uassert.Equal(t, "abab", facade.Call(cross(cur), "repeat", "ab|2")) // The facade checks arity before the handler runs, so the message names the // signature rather than whatever the handler would have done. uassert.AbortsContains(t, cur, "verb repeat takes 2 argument(s), got 1, signature is repeat(s, n)", func() { facade.Call(cross(cur), "repeat", "ab") }) uassert.AbortsContains(t, cur, "unknown verb nope, known: upper, repeat", func() { facade.Call(cross(cur), "nope", "") }) // v1 ADDS a verb and keeps the other two, so it is accepted. No new realm, // no new signature at the permanent path, and old callers are untouched. facade.Accept(cross(cur), Path) uassert.Equal(t, "upper, repeat, surround", joinVerbs(facade.Verbs())) uassert.Equal(t, "[gno]", facade.Call(cross(cur), "surround", "gno|[|]")) uassert.Equal(t, "GNO", facade.Call(cross(cur), "upper", "gno"), "v0's verbs still answer") // The refusal. bad satisfies Handler exactly as well as the others do; what // it does is drop a verb and reshape another, which only the schema sees. uassert.AbortsContains(t, cur, "schema regression: the candidate drops verb upper", func() { facade.Accept(cross(cur), bad.Path) }) uassert.Equal(t, Path, facade.Live(), "a refused accept changes nothing") // The diff is SYMMETRIC, which the first draft of this test got wrong: now // that v1 has added a verb, rolling back to v0 DROPS it and is a regression // by the same rule. Accept refuses the rollback. uassert.AbortsContains(t, cur, "schema regression: the candidate drops verb surround", func() { facade.Accept(cross(cur), implv0.Path) }) uassert.Equal(t, Path, facade.Live()) // Which is why the override exists. A pattern that can only move forward is // worse than one with no diff at all. facade.AcceptBreaking(cross(cur), implv0.Path) uassert.Equal(t, "upper, repeat", joinVerbs(facade.Verbs())) facade.Accept(cross(cur), Path) uassert.Equal(t, "upper s\nrepeat s n\nsurround s left right\n", facade.SchemaText()) uassert.Equal(t, "schema/facade/v0: gno.land/r/moul/x/upgrade/schema/impl/v1\n"+ "- upper(s)\n- repeat(s, n)\n- surround(s, left, right)\n", facade.Render("")) } // TestDispatchCost100 does 100 dispatches through this pattern's entry point, // so `gno test -print-runtime-metrics` prints a cycle count for a known // workload. Its twin is TestDispatchCost100 in adminreg/impl/gen1. // // It measures what a call costs through each pattern AS SHIPPED, which is the // number that decides between them. It is NOT the cost of the string boundary // in isolation: this path also crosses a realm and the typed one does not. func TestDispatchCost100(cur realm, t *testing.T) { // Runs after TestGrowTheAPI, so v1 is already live. Accepting anything here // would fight that test's state, and the verb costs the same either way. for i := 0; i < 100; i++ { facade.Call(cross(cur), "upper", "gno") } uassert.Equal(t, "GNO", facade.Call(cross(cur), "upper", "gno")) } // TestMalformedSchemaIsRejectedAtProposal checks the other validation edge: a // schema that cannot be parsed never becomes a candidate. func TestMalformedSchemaIsRejectedAtProposal(cur realm, t *testing.T) { uassert.AbortsContains(t, cur, "malformed schema", func() { facade.Propose(cross(cur), badSchema{}) }) } type badSchema struct{} func (badSchema) Schema() string { return "Upper s" } // capital, not [a-z0-9_] func (badSchema) Invoke(string, []string) string { return "" } func joinVerbs(v []string) string { out := "" for i, s := range v { if i > 0 { out += ", " } out += s } return out }
Result log
msg:0,success:true,log:,events:[] msg:1,success:true,log:,events:[] msg:2,success:true,log:,events:[] msg:3,success:true,log:,events:[]