Transaction
BD9E746874A08B…2245881D5867
Block 265,720 · index 0 · indexed
Summary
- Hash
- BD9E746874A08B7C2103160F8347BBBB3DC79682EA4AB94AC4782245881D5867
- Block
- 265,720
- Size
- 22529 bytes
- Gas used
- 33,926,134 / 60,000,000
- Fee
- 120000 ugnot
- Status
- success
Messages
- Attached funds
- 9000000 ugnot
Arguments · 13
- #1memba_arcade_leaderboard_v1
- #2admin.gno
- #3package memba_arcade_leaderboard_v1 import "chain" // Admin surface — owner-gated. The owner is the samcrew multisig at launch and can be handed to // the memba_dao executor via the 2-step TransferOwnership/AcceptOwnership (never a 1-step transfer // to a mistyped/dead address). // AddAttester allowlists an ADDRESS (the backend verifier's dedicated low-privilege key — never the // deploy multisig itself) as an authorized result source. Owner only. func AddAttester(cur realm, addr address) { assertOwner(cur) if addr == "" { panic("attester address must be non-empty") } attesters.Set(addr.String(), true) chain.Emit("AttesterAdded", "addr", addr.String()) } // RemoveAttester revokes an attester key. Owner only. func RemoveAttester(cur realm, addr address) { assertOwner(cur) attesters.Remove(addr.String()) chain.Emit("AttesterRemoved", "addr", addr.String()) } // Pause freezes all attestation + receipt requests (reads stay available). Owner only. func Pause(cur realm, state bool) { assertOwner(cur) paused = state chain.Emit("PauseSet", "paused", boolStr(state)) } // TransferOwnership stages a new owner; it takes effect only after AcceptOwnership is called BY // that address (2-step, so a typo can't brick admin). func TransferOwnership(cur realm, newOwner address) { assertOwner(cur) if newOwner == "" { panic("newOwner must be non-empty") } if !newOwner.IsValid() { panic("invalid address: " + newOwner.String()) } if newOwner == owner { panic("newOwner is the current owner") } pendingOwner = newOwner chain.Emit("OwnershipTransferStarted", "pending", newOwner.String()) } // CancelOwnershipTransfer withdraws a staged handoff. Owner only. func CancelOwnershipTransfer(cur realm) { assertOwner(cur) if pendingOwner == "" { panic("no pending ownership transfer") } cancelled := pendingOwner pendingOwner = "" chain.Emit("OwnershipTransferCancelled", "pending", cancelled.String()) } // GetPendingOwner returns the staged successor, or the empty address. func GetPendingOwner() address { return pendingOwner } // AcceptOwnership completes the handoff. Only the staged pendingOwner may call it. func AcceptOwnership(cur realm) { if !cur.IsCurrent() { panic("spoofed realm") } if pendingOwner == "" { panic("no pending ownership transfer") } if cur.Previous().Address() != pendingOwner { panic("unauthorized: only the pending owner may accept") } owner = pendingOwner pendingOwner = "" chain.Emit("OwnershipTransferAccepted", "owner", owner.String()) }
- #4gnomod.toml
- #5module = "gno.land/r/samcrew/memba_arcade_leaderboard_v1" gno = "0.9"
- #6leaderboard.gno
- #7// Package memba_arcade_leaderboard_v1 is the funds-free, verified-score ledger for the Memba // arcade (BARRICADE, Space Invaders, and future games — one realm, boards keyed per game slug). // Nothing here trusts a client number: an authorized ATTESTER (the backend verifier's key) // re-simulates a submitted input log and attests only the reproduced result. The chain stores the // attested result plus a commitment to the log (its sha256) — the log itself stays off-chain; // "verified, never trustless" is the public contract. // // SAFETY: there is NO banker, NO transfer, and NO OriginSend anywhere — the fund-drain risk class // does not apply. The only trust boundary is who may attest (owner ∪ the attester allowlist); every // write emits an event. Two buckets never mix: the competitive DAILY BOARD (one best entry per // address per game per day, shared seed) and personal TRAINING RECEIPTS (practice runs — a // verified flex, never ranked). // // The game slug is shape-validated only — there is deliberately NO on-chain game registry: the // realm is funds-free, so the worst a typo'd slug can do is fragment a board, and the attester // (not the player) authors the slug. package memba_arcade_leaderboard_v1 import ( "chain" "chain/runtime" "gno.land/p/samcrew/avl" "gno.land/p/nt/ufmt/v0" ) // No owner is compiled in: the publishing transaction's signer (on gnoland-1 the samcrew // namespace multisig, the stamped creator at enable time) is seeded as owner at package load; // hand it to the memba_dao executor post-deploy via the 2-step ownership transfer. const ( // MaxScore bounds a single attested score — a fat-finger / overflow guard far above any // reachable sim score (arcade sim scores live in the tens of thousands). MaxScore = int64(1_000_000_000) // MaxGameLen bounds the game slug. The slug charset is [a-z0-9-], which EXCLUDES '|' — // load-bearing: '|' is the composite-key separator in every scoped tree below. MaxGameLen = 32 // MaxStatsLen bounds the attester-authored per-game stats JSON blob. Ranking never reads // it; reads only ever emit it JSON-escaped. MaxStatsLen = 256 // ReceiptsCap bounds one address's training-receipt ring (oldest evicted first; the ring is // mixed-game, so this is the per-address bound). ReceiptsCap = 100 // ReceiptDailyCap bounds how many receipts one address may mint per (game, day) (spam cap). ReceiptDailyCap = 5 // RequestCap bounds one address's PENDING receipt requests (fulfilled ones free their slot): // the only unprivileged write path must not grow realm state unboundedly. RequestCap = 8 ) // Entry is one attested, verifier-reproduced run result. type Entry struct { Addr address Game string // slug, 1..MaxGameLen chars of [a-z0-9-] (never '|', the key separator) Day string // YYYY-MM-DD (the shared daily seed's date; receipt = submission day) Mode string // "daily" | "practice" Seed string Score int64 // the ONLY ranked field Stats string // compact attester-authored JSON (game-specific; ≤ MaxStatsLen; opaque here) SimVersion int64 StateHash string // the sim's canonical terminal-state digest InputLogSha256 string // commitment to the off-chain input log (the proof) AttestedAt int64 // block height } var ( owner address pendingOwner address paused bool attesters = avl.NewTree() // attester address string -> bool boards = avl.NewTree() // "game|day|addr" -> *Entry (competitive, one best per addr/game/day) receipts = avl.NewTree() // addr string -> []*Entry (training ring, mixed-game, capped) logHashes = avl.NewTree() // "game|day|sha256" -> addr string (board slot: first-submitter-per-day) hashOwners = avl.NewTree() // sha256 -> addr string (GLOBAL: a log binds to ONE address, ever) requests = avl.NewTree() // "addr|sha256" -> int64 height (player-paid receipt requests) requestCount = avl.NewTree() // addr string -> int (pending requests, capped at RequestCap) boardCount = avl.NewTree() // "game|day" -> int (entries per game-day, O(1) reads) ) func init() { seedAuthority(publisherAtLoad()) } // seedAuthority installs the publisher as owner. It runs once at package load. func seedAuthority(publisher address) { owner = publisher pendingOwner = "" } func assertOwner(cur realm) { if !cur.IsCurrent() { panic("spoofed realm") } if cur.Previous().Address() != owner { panic("unauthorized: owner only") } } func assertNotPaused() { if paused { panic("attestation is paused") } } func isAttester(addr string) bool { _, ok := attesters.Get(addr) return ok } // assertAttester is the single trust boundary: only the owner or an allowlisted attester key (the // backend verifier) may write results. The verifier's own re-simulation gate is what makes an // attested score meaningful — this realm only enforces that the source is trusted. func assertAttester(cur realm) { if !cur.IsCurrent() { panic("spoofed realm") } pr := cur.Previous().Address() if pr != owner && !isAttester(pr.String()) { panic("unauthorized: owner or allowlisted attester only") } } func assertEntryShape(game string, addr address, day, mode, seed string, score, simVersion int64, stateHash, logHash, stats string) { if addr == "" { panic("addr must be non-empty") } assertGameSlug(game) if len(day) != 10 || day[4] != '-' || day[7] != '-' { panic("day must be YYYY-MM-DD") } for _, i := range []int{0, 1, 2, 3, 5, 6, 8, 9} { if day[i] < '0' || day[i] > '9' { panic("day must be YYYY-MM-DD") } } if mode != "daily" && mode != "practice" { panic("mode must be daily or practice") } if seed == "" { panic("seed must be non-empty") } if score < 0 || score > MaxScore { panic("score out of range") } if simVersion <= 0 { panic("simVersion must be positive") } if stateHash == "" || logHash == "" { panic("stateHash and inputLogSha256 must be non-empty") } if len(stats) > MaxStatsLen { panic("stats too long") } } // assertGameSlug validates the game slug shape: 1..MaxGameLen bytes of [a-z0-9-]. The charset // excludes '|' (and everything else), so a slug can never forge or split a composite key. func assertGameSlug(game string) { if len(game) < 1 || len(game) > MaxGameLen { panic("game must be 1-32 chars of [a-z0-9-]") } for i := 0; i < len(game); i++ { c := game[i] if (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '-' { panic("game must be 1-32 chars of [a-z0-9-]") } } } // AttestScore writes (or improves) an address's entry on a game's competitive board for a day. // Attester-only. The verifier batches a day's results into one multi-msg tx at day close, plus // spam-capped singles for the "certify now" flow. Re-attesting the same game/address/day only ever // RAISES the score (Second Wind). A log hash already attested for that game-day for a DIFFERENT // address is rejected — on a shared daily seed a valid input log is portable, so first-submitter // wins (replay-theft defense). `stats` is the attester-authored per-game JSON blob (waves, wins, // levels, …) — never ranked, only echoed by reads. func AttestScore(cur realm, game string, addr address, day, seed string, score, simVersion int64, stateHash, logHash, stats string) { assertNotPaused() assertAttester(cur) assertEntryShape(game, addr, day, "daily", seed, score, simVersion, stateHash, logHash, stats) hashKey := game + "|" + day + "|" + logHash if v, ok := logHashes.Get(hashKey); ok && v.(string) != addr.String() { panic("duplicate input log: already attested for another address today") } // The GLOBAL net: a log binds to one address forever, across BOTH buckets — // a stolen log can't take a board slot today, a receipt tomorrow, or // anything under a different day OR GAME string (the review rationale // generalizes: no free-form string may launder a stolen log, so this tree // deliberately stays UNSCOPED by game). assertHashOwner(logHash, addr) key := game + "|" + day + "|" + addr.String() if v, ok := boards.Get(key); ok { if v.(*Entry).Score >= score { panic("existing entry is not improved") } } else { countKey := game + "|" + day n := 0 if c, ok := boardCount.Get(countKey); ok { n = c.(int) } boardCount.Set(countKey, n+1) } e := &Entry{ Addr: addr, Game: game, Day: day, Mode: "daily", Seed: seed, Score: score, Stats: stats, SimVersion: simVersion, StateHash: stateHash, InputLogSha256: logHash, AttestedAt: runtime.ChainHeight(), } boards.Set(key, e) logHashes.Set(hashKey, addr.String()) hashOwners.Set(logHash, addr.String()) chain.Emit("ScoreAttested", "game", game, "addr", addr.String(), "day", day, "score", itoa64(score), "simVersion", itoa64(simVersion)) } // AttestReceipt mints a personal training receipt — a verified "I did this run", never ranked. // Attester-only; capped per address per (game, day) and ring-capped per address. Fulfills (and // clears) a matching player-paid RequestReceipt if one is staged. func AttestReceipt(cur realm, game string, addr address, day, seed string, score, simVersion int64, stateHash, logHash, stats string) { assertNotPaused() assertAttester(cur) assertEntryShape(game, addr, day, "practice", seed, score, simVersion, stateHash, logHash, stats) var ring []*Entry if v, ok := receipts.Get(addr.String()); ok { ring = v.([]*Entry) } sameDay := 0 for _, r := range ring { if r.InputLogSha256 == logHash { panic("duplicate receipt for this input log") } if r.Game == game && r.Day == day { sameDay++ } } if sameDay >= ReceiptDailyCap { panic("receipt cap reached for this day") } // The same GLOBAL net as the board: a receipt for someone else's attested // log is a forged "verified flex" — reject it, and bind first use here too. assertHashOwner(logHash, addr) e := &Entry{ Addr: addr, Game: game, Day: day, Mode: "practice", Seed: seed, Score: score, Stats: stats, SimVersion: simVersion, StateHash: stateHash, InputLogSha256: logHash, AttestedAt: runtime.ChainHeight(), } ring = append(ring, e) if len(ring) > ReceiptsCap { ring = ring[len(ring)-ReceiptsCap:] // evict oldest } receipts.Set(addr.String(), ring) hashOwners.Set(logHash, addr.String()) clearRequest(addr, logHash) chain.Emit("ReceiptAttested", "game", game, "addr", addr.String(), "day", day, "score", itoa64(score), "simVersion", itoa64(simVersion)) } // RequestReceipt is the player-paid leg: ANY signer stages an on-chain consent + payment anchor for // one of their own runs (they pay this call's gas). The backend sees the event, verifies the stored // log, and fulfills via AttestReceipt. Self-limiting: each request costs the caller gas, and one // key slot per (addr, hash). Hash-scoped and game-agnostic on purpose — the verifier, not the // player, decides which game the log replays under. func RequestReceipt(cur realm, logHash string) { assertNotPaused() if !cur.IsCurrent() { panic("spoofed realm") } if logHash == "" { panic("logHash must be non-empty") } caller := cur.Previous().Address() key := caller.String() + "|" + logHash if _, ok := requests.Get(key); !ok { n := 0 if c, ok := requestCount.Get(caller.String()); ok { n = c.(int) } if n >= RequestCap { panic("pending request cap reached — wait for fulfillment") } requestCount.Set(caller.String(), n+1) } requests.Set(key, runtime.ChainHeight()) chain.Emit("ReceiptRequested", "addr", caller.String(), "logHash", logHash) } // assertHashOwner enforces the GLOBAL one-log-one-address rule shared by both // buckets (and by every game), and is idempotent for the rightful owner // (Second Wind re-attests). func assertHashOwner(logHash string, addr address) { if v, ok := hashOwners.Get(logHash); ok && v.(string) != addr.String() { panic("duplicate input log: already bound to another address") } } // clearRequest removes a fulfilled request and frees the caller's cap slot. func clearRequest(addr address, logHash string) { key := addr.String() + "|" + logHash if _, ok := requests.Get(key); !ok { return } requests.Remove(key) if c, ok := requestCount.Get(addr.String()); ok { if n := c.(int); n > 1 { requestCount.Set(addr.String(), n-1) } else { requestCount.Remove(addr.String()) } } } func itoa64(n int64) string { return ufmt.Sprintf("%d", n) } func boolStr(b bool) string { if b { return "true" } return "false" }
- #8publisher.gno
- #9package memba_arcade_leaderboard_v1 import "chain/runtime/unsafe" // publisherAtLoad captures the AddPackage signer during package initialization. // Keeping the unsafe load-time primitive separate from crossing entrypoints lets // privileged calls use only the frame-verified cur.IsCurrent/cur.Previous API. func publisherAtLoad() address { return unsafe.OriginCaller() }
- #10reads.gno
- #11package memba_arcade_leaderboard_v1 import ( "strings" "gno.land/p/nt/ufmt/v0" ) // ── Read getters (pure, non-failing; served via vm/qeval or Render) ───────────── // GetOwner returns the current owner. func GetOwner() address { return owner } // IsPaused reports whether attestation is frozen. func IsPaused() bool { return paused } // IsAttester reports whether an address is on the attester allowlist. func IsAttester(addr string) bool { return isAttester(addr) } // BoardCount returns how many addresses hold an entry on a game's board for a day (O(1)). func BoardCount(game, day string) int { if v, ok := boardCount.Get(game + "|" + day); ok { return v.(int) } return 0 } // entryJSON emits one entry. `stats` is the attester's opaque JSON blob and is emitted as an // ESCAPED STRING FIELD — never spliced raw into the document — so a malformed blob can corrupt // nothing but itself. func entryJSON(e *Entry) string { return ufmt.Sprintf( `{"addr":"%s","game":"%s","day":"%s","mode":"%s","score":%d,"stats":"%s","simVersion":%d,"stateHash":"%s","inputLogSha256":"%s","attestedAt":%d}`, jsonEscape(e.Addr.String()), jsonEscape(e.Game), jsonEscape(e.Day), jsonEscape(e.Mode), e.Score, jsonEscape(e.Stats), e.SimVersion, jsonEscape(e.StateHash), jsonEscape(e.InputLogSha256), e.AttestedAt) } // GetEntryJSON returns one address's board entry for a game-day, or "null". func GetEntryJSON(game, day, addr string) string { if v, ok := boards.Get(game + "|" + day + "|" + addr); ok { return entryJSON(v.(*Entry)) } return "null" } // dayEntries collects a game-day's board entries in canonical rank order: score desc, // then earlier attestation, then address — a total order, so pagination is stable. func dayEntries(game, day string) []*Entry { var out []*Entry prefix := game + "|" + day + "|" boards.Iterate(prefix, prefix+"~", func(_ string, v any) bool { out = append(out, v.(*Entry)) return false }) // Insertion sort (gno stdlib has no sort.Slice): a day's board is bounded // and reads are qeval-side — O(n²) on a few hundred entries is fine. for i := 1; i < len(out); i++ { e := out[i] j := i - 1 for j >= 0 && ranksAfter(out[j], e) { out[j+1] = out[j] j-- } out[j+1] = e } return out } // ranksAfter reports whether a should sit AFTER b in rank order — a total // order (score desc, then earlier attestation, then address) so pagination // is stable and identical everywhere. func ranksAfter(a, b *Entry) bool { if a.Score != b.Score { return a.Score < b.Score } if a.AttestedAt != b.AttestedAt { return a.AttestedAt > b.AttestedAt } return a.Addr.String() > b.Addr.String() } // GetBoardJSON returns a page of a game-day's board, rank-ordered, as // {"game":…,"day":…,"total":N,"entries":[…]}. Reads sort the day's entries on demand — // fine for a testnet-scale board; a rank index (points_v1-style) is the v2 // upgrade if daily boards grow past a few thousand entries. func GetBoardJSON(game, day string, offset, limit int) string { all := dayEntries(game, day) if offset < 0 { offset = 0 } if limit <= 0 || limit > 100 { limit = 100 } var sb strings.Builder sb.WriteString(ufmt.Sprintf(`{"game":"%s","day":"%s","total":%d,"entries":[`, jsonEscape(game), jsonEscape(day), len(all))) for i := offset; i < len(all) && i < offset+limit; i++ { if i > offset { sb.WriteString(",") } sb.WriteString(entryJSON(all[i])) } sb.WriteString("]}") return sb.String() } // GetReceiptsJSON returns a page of one address's training receipts (all games, newest first) as // {"addr":…,"total":N,"entries":[…]}. func GetReceiptsJSON(addr string, offset, limit int) string { var ring []*Entry if v, ok := receipts.Get(addr); ok { ring = v.([]*Entry) } if offset < 0 { offset = 0 } if limit <= 0 || limit > 100 { limit = 100 } var sb strings.Builder sb.WriteString(ufmt.Sprintf(`{"addr":"%s","total":%d,"entries":[`, jsonEscape(addr), len(ring))) n := 0 for i := len(ring) - 1 - offset; i >= 0 && n < limit; i-- { if n > 0 { sb.WriteString(",") } sb.WriteString(entryJSON(ring[i])) n++ } sb.WriteString("]}") return sb.String() } // Render serves a human-readable snapshot: `:board/<game>/YYYY-MM-DD` for a game-day's top // entries, `:receipts/<addr>` for an address's latest receipts. func Render(path string) string { if strings.HasPrefix(path, "board/") { rest := path[len("board/"):] slash := strings.Index(rest, "/") if slash < 0 { return "# memba_arcade_leaderboard_v1\n\nUse `:board/<game>/YYYY-MM-DD`.\n" } game := rest[:slash] day := rest[slash+1:] all := dayEntries(game, day) var sb strings.Builder sb.WriteString("# " + jsonEscape(game) + " daily board — " + jsonEscape(day) + "\n\n") if len(all) == 0 { sb.WriteString("No attested runs yet.\n") return sb.String() } for i, e := range all { if i >= 50 { sb.WriteString(ufmt.Sprintf("\n…and %d more.\n", len(all)-50)) break } line := ufmt.Sprintf("%d. `%s` — **%d**", i+1, e.Addr.String(), e.Score) if e.Stats != "" { line += " · " + jsonEscape(e.Stats) } sb.WriteString(line + "\n") } return sb.String() } if strings.HasPrefix(path, "receipts/") { addr := path[len("receipts/"):] return "# Training receipts — " + addr + "\n\n```json\n" + GetReceiptsJSON(addr, 0, 20) + "\n```\n" } return "# memba_arcade_leaderboard_v1\n\nVerified Memba arcade results — never a client-claimed number.\n\n" + "- `:board/<game>/YYYY-MM-DD` — a game's competitive board for a day\n" + "- `:receipts/<addr>` — an address's training receipts (all games)\n" } // jsonEscape escapes a string for embedding in a JSON string literal (defensive — addresses are // bech32 and hashes are hex, but the read path escapes regardless; the Render path reuses it to // neutralize newlines/control chars in attester-authored stats). func jsonEscape(s string) string { var sb strings.Builder for _, r := range s { switch r { case '"': sb.WriteString("\\\"") case '\\': sb.WriteString("\\\\") case '\n': sb.WriteString("\\n") case '\r': sb.WriteString("\\r") case '\t': sb.WriteString("\\t") default: if r < 0x20 { sb.WriteString(ufmt.Sprintf("\\u%04x", r)) } else { sb.WriteString(string(r)) } } } return sb.String() }
- #12/gno.MemPackageType
- #13 MPUserAll
Result log
msg:0,success:true,log:,events:[]