{"jsonrpc":"2.0","id":"","result":{"genesis":{"genesis_time":"2026-09-08T02:03:46Z","chain_id":"kourt-1","consensus_params":{"Block":{"MaxTxBytes":"1000000","MaxDataBytes":"2000000","MaxBlockBytes":"0","MaxGas":"3000000000","TimeIotaMS":"100"},"Validator":{"PubKeyTypeURLs":["/tm.PubKeyEd25519"]}},"validators":[{"address":"g1al98775jkmc7s5q4usamsnf5qegcgtna8xt0my","pub_key":{"@type":"/tm.PubKeyEd25519","value":"EFt2dEyfy14ghkvLhrxBEZxN+42OhUg+5bxp5TU6lII="},"power":"1","name":"kourt0"}],"app_hash":null,"app_state":{"@type":"/gno.GenesisState","auth":{"params":{"max_memo_bytes":"65536","tx_sig_limit":"7","tx_size_cost_per_byte":"10","sig_verify_cost_ed25519":"590","sig_verify_cost_secp256k1":"1000","gas_price_change_compressor":"10","target_gas_ratio":"70","initial_gasprice":{"gas":"1000","price":"1ugnot"},"unrestricted_addrs":null,"fee_collector":"g17xpfvakm2amg962yls6f84z3kell8c5lr9lr2e"}},"bank":{"params":{"restricted_denoms":[]}},"vm":{"params":{"sysnames_pkgpath":"gno.land/r/sys/names","syscla_pkgpath":"gno.land/r/sys/cla","chain_domain":"gno.land","default_deposit":"600000000ugnot","storage_price":"100ugnot","storage_fee_collector":"g1c9stkafpvcwez2efq3qtfuezw4zpaux3tvxggk","min_get_read_depth_100":"100","min_set_read_depth_100":"200","min_write_depth_100":"540","fixed_get_read_depth_100":"100","fixed_set_read_depth_100":"200","fixed_write_depth_100":"540","iter_next_cost_flat":"1000","preprocess_gas_per_byte":"1250"},"realm_params":null},"balances":["g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy=2304000000ugnot","g1yxfv5xkdlht9jc9kaeqku29jfgwxsj2knmv6ym=1872000000ugnot","g1ygrl7wtdvqk9jap5k42vhg0c06vun48qpdg7wa=2160000000ugnot","g18l9f69nynlexm46sky50pshpcvycnrrcywxavr=2736000000ugnot","g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms=2880000000ugnot","g1w746drdmenjdg0ll38dltjt7kkgtq5lmsmghcg=1000000000000ugnot","g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy=2160000000ugnot","g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz=2592000000ugnot","g1j5400n4ra3pzu849lvq2s2uwwhn7hs8cu7wz7t=5000000000000ugnot","g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7=2016000000ugnot","g1nrhrxfcz89l9psh44cmc30mjfds8ukn023r2ts=3024000000ugnot","g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v=2016000000ugnot","g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv=2448000000ugnot","g1c88ydxxf56gpvns0pemggj536yct7vpc9556ac=1584000000ugnot","g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl=1872000000ugnot","g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc=2160000000ugnot","g1ap7p83he8hxzysla0z3gcpvvy72jgkvqrh6wkl=2448000000ugnot","g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz=10000000000000ugnot","g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y=3024000000ugnot"],"txs":[{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","package":{"name":"bptree","path":"gno.land/p/nt/bptree/v0","files":[{"name":"doc.gno","body":"// Package bptree provides a mutable B+ tree implementation for storing\n// key-value data in Gno realms. It implements the same ITree interface\n// as the avl package but uses a B+ tree internally for better cache\n// locality and fewer pointer dereferences per operation.\n//\n// The fanout (maximum number of children per inner node, and maximum\n// number of entries per leaf node) is configurable:\n//\n//\ttree := bptree.NewBPTree32()    // fanout 32\n//\ttree := bptree.NewBPTreeN(64)   // fanout 64\n//\n// The zero value is usable as an empty tree with fanout 32:\n//\n//\tvar tree bptree.BPTree\n//\ttree.Set(\"key\", \"value\")\npackage bptree\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/bptree/v0\"\ngno = \"0.9\"\n"},{"name":"node.gno","body":"package bptree\n\n// node is the interface satisfied by both inner and leaf nodes.\ntype node interface {\n\tisLeaf() bool\n\tnodeSize() int // total leaf entries in subtree\n\tminKey() string\n}\n\n//----------------------------------------\n// leafNode\n\ntype leafNode struct {\n\tkeys   []string\n\tvalues []*any // each value is a separate object for lazy loading\n}\n\nfunc newLeafNode(fanout int) *leafNode {\n\treturn \u0026leafNode{\n\t\tkeys:   make([]string, 0, fanout),\n\t\tvalues: make([]*any, 0, fanout),\n\t}\n}\n\nfunc (n *leafNode) isLeaf() bool   { return true }\nfunc (n *leafNode) nodeSize() int  { return len(n.keys) }\nfunc (n *leafNode) minKey() string { return n.keys[0] }\n\n// find returns the index where key is or would be inserted,\n// and whether an exact match was found.\nfunc (n *leafNode) find(key string) (int, bool) {\n\tlo, hi := 0, len(n.keys)\n\tfor lo \u003c hi {\n\t\tmid := lo + (hi-lo)/2\n\t\tif n.keys[mid] \u003c key {\n\t\t\tlo = mid + 1\n\t\t} else {\n\t\t\thi = mid\n\t\t}\n\t}\n\tif lo \u003c len(n.keys) \u0026\u0026 n.keys[lo] == key {\n\t\treturn lo, true\n\t}\n\treturn lo, false\n}\n\n// insertAt inserts a key-value pair at the given position.\nfunc (n *leafNode) insertAt(pos int, key string, value *any) {\n\tn.keys = append(n.keys, \"\")\n\tcopy(n.keys[pos+1:], n.keys[pos:])\n\tn.keys[pos] = key\n\n\tn.values = append(n.values, nil)\n\tcopy(n.values[pos+1:], n.values[pos:])\n\tn.values[pos] = value\n}\n\n// removeAt removes the entry at the given position and returns the removed key and value.\nfunc (n *leafNode) removeAt(pos int) (string, *any) {\n\tkey := n.keys[pos]\n\tvalue := n.values[pos]\n\n\tcopy(n.keys[pos:], n.keys[pos+1:])\n\tn.keys[len(n.keys)-1] = \"\"\n\tn.keys = n.keys[:len(n.keys)-1]\n\n\tcopy(n.values[pos:], n.values[pos+1:])\n\tn.values[len(n.values)-1] = nil\n\tn.values = n.values[:len(n.values)-1]\n\n\treturn key, value\n}\n\n//----------------------------------------\n// innerNode\n\ntype innerNode struct {\n\tkeys     []string // separator keys; keys[i] = minKey of children[i+1]\n\tchildren []node\n\tsizes    []int // sizes[i] = total leaf entries in children[i]\n}\n\nfunc newInnerNode(fanout int) *innerNode {\n\treturn \u0026innerNode{\n\t\tkeys:     make([]string, 0, fanout-1),\n\t\tchildren: make([]node, 0, fanout),\n\t\tsizes:    make([]int, 0, fanout),\n\t}\n}\n\nfunc (n *innerNode) isLeaf() bool { return false }\nfunc (n *innerNode) nodeSize() int {\n\tsum := 0\n\tfor _, s := range n.sizes {\n\t\tsum += s\n\t}\n\treturn sum\n}\nfunc (n *innerNode) minKey() string { return n.children[0].minKey() }\n\n// findChild returns the child index for the given key.\nfunc (n *innerNode) findChild(key string) int {\n\t// Binary search: find the rightmost i where keys[i] \u003c= key.\n\tlo, hi := 0, len(n.keys)\n\tfor lo \u003c hi {\n\t\tmid := lo + (hi-lo)/2\n\t\tif n.keys[mid] \u003c= key {\n\t\t\tlo = mid + 1\n\t\t} else {\n\t\t\thi = mid\n\t\t}\n\t}\n\treturn lo\n}\n\n// insertChildAt inserts a new child with its separator key at the given position.\n// The separator key is placed at keys[pos-1] (since keys[i] = minKey of children[i+1]).\nfunc (n *innerNode) insertChildAt(pos int, sep string, child node, sz int) {\n\t// Insert separator at keys[pos-1].\n\tkeyPos := pos - 1\n\tn.keys = append(n.keys, \"\")\n\tcopy(n.keys[keyPos+1:], n.keys[keyPos:])\n\tn.keys[keyPos] = sep\n\n\t// Insert child at children[pos].\n\tn.children = append(n.children, nil)\n\tcopy(n.children[pos+1:], n.children[pos:])\n\tn.children[pos] = child\n\n\t// Insert size at sizes[pos].\n\tn.sizes = append(n.sizes, 0)\n\tcopy(n.sizes[pos+1:], n.sizes[pos:])\n\tn.sizes[pos] = sz\n}\n\n// removeChildAt removes the child at pos and its associated separator key.\nfunc (n *innerNode) removeChildAt(pos int) {\n\t// Determine which separator to remove.\n\t// keys[i] separates children[i] from children[i+1].\n\t// Removing children[pos]: if pos \u003e 0, remove keys[pos-1]; else remove keys[0].\n\tkeyPos := pos\n\tif pos \u003e 0 {\n\t\tkeyPos = pos - 1\n\t}\n\tif len(n.keys) \u003e 0 {\n\t\tcopy(n.keys[keyPos:], n.keys[keyPos+1:])\n\t\tn.keys[len(n.keys)-1] = \"\"\n\t\tn.keys = n.keys[:len(n.keys)-1]\n\t}\n\n\tcopy(n.children[pos:], n.children[pos+1:])\n\tn.children[len(n.children)-1] = nil\n\tn.children = n.children[:len(n.children)-1]\n\n\tcopy(n.sizes[pos:], n.sizes[pos+1:])\n\tn.sizes = n.sizes[:len(n.sizes)-1]\n}\n"},{"name":"tree.gno","body":"package bptree\n\ntype ITree interface {\n\tSize() int\n\tHas(key string) bool\n\tGet(key string) any\n\tGetByIndex(index int) (key string, value any)\n\tIterate(start, end string, cb IterCbFn) bool\n\tReverseIterate(start, end string, cb IterCbFn) bool\n\tIterateByOffset(offset int, count int, cb IterCbFn) bool\n\tReverseIterateByOffset(offset int, count int, cb IterCbFn) bool\n\tSet(key string, value any) (updated bool)\n\tRemove(key string) (value any, removed bool)\n}\n\ntype IterCbFn func(key string, value any) bool\n\n// Verify BPTree implements ITree.\nvar _ ITree = (*BPTree)(nil)\n\n// The zero value is usable as an empty tree with fanout 32.\ntype BPTree struct {\n\troot   node\n\tsize   int\n\tfanout int\n}\n\n// NewBPTreeN creates a new empty B+ tree with the given fanout.\n// It panics when fanout is lower than 4.\nfunc NewBPTreeN(fanout int) *BPTree {\n\tif fanout \u003c 4 {\n\t\tpanic(\"bptree: fanout must be \u003e= 4\")\n\t}\n\treturn \u0026BPTree{fanout: fanout}\n}\n\n// NewBPTree32 creates a new empty B+ tree with fanout 32.\nfunc NewBPTree32() *BPTree {\n\treturn NewBPTreeN(32)\n}\n\nfunc (t *BPTree) Size() int {\n\treturn t.size\n}\n\nfunc (t *BPTree) Has(key string) bool {\n\tif t.root == nil {\n\t\treturn false\n\t}\n\tn := t.root\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tidx := inner.findChild(key)\n\t\tn = inner.children[idx]\n\t}\n\tleaf := n.(*leafNode)\n\t_, found := leaf.find(key)\n\treturn found\n}\n\n// Get retrieves the value associated with the given key.\n// It returns the value if the key exists, or nil if it doesn't.\n// This allows for a simpler usage pattern with type assertions:\n//\n//\tif value, ok := tree.Get(\"key\").(MyType); ok {\n//\t    // use value\n//\t}\n//\n// Use Has to distinguish a stored nil value from a missing key.\nfunc (t *BPTree) Get(key string) any {\n\tif t.root == nil {\n\t\treturn nil\n\t}\n\tn := t.root\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tidx := inner.findChild(key)\n\t\tn = inner.children[idx]\n\t}\n\tleaf := n.(*leafNode)\n\tpos, found := leaf.find(key)\n\tif !found {\n\t\treturn nil\n\t}\n\treturn *leaf.values[pos]\n}\n\n// GetByIndex returns the key-value pair at the given 0-based index.\n// Panics if index is out of range.\nfunc (t *BPTree) GetByIndex(index int) (key string, value any) {\n\tif t.root == nil || index \u003c 0 || index \u003e= t.size {\n\t\tpanic(\"GetByIndex asked for invalid index\")\n\t}\n\tn := t.root\n\trem := index\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tfound := false\n\t\tfor i, s := range inner.sizes {\n\t\t\tif rem \u003c s {\n\t\t\t\tn = inner.children[i]\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\trem -= s\n\t\t}\n\t\tif !found {\n\t\t\tpanic(\"GetByIndex asked for invalid index\")\n\t\t}\n\t}\n\tleaf := n.(*leafNode)\n\treturn leaf.keys[rem], *leaf.values[rem]\n}\n\n//----------------------------------------\n// Set\n\n// pathEntry records a step in the root-to-leaf descent.\ntype pathEntry struct {\n\tinner    *innerNode\n\tchildIdx int\n}\n\n// Set inserts or updates a key-value pair. Returns true if the key already existed.\nfunc (t *BPTree) Set(key string, value any) (updated bool) {\n\tif t.fanout == 0 {\n\t\tt.fanout = 32\n\t}\n\tfanout := t.fanout\n\n\tvp := \u0026value // wrap value in *any for lazy loading\n\n\t// Empty tree: create a single leaf.\n\tif t.root == nil {\n\t\tleaf := newLeafNode(fanout)\n\t\tleaf.keys = append(leaf.keys, key)\n\t\tleaf.values = append(leaf.values, vp)\n\t\tt.root = leaf\n\t\tt.size = 1\n\t\treturn false\n\t}\n\n\t// Descend to the leaf, recording the path.\n\tvar path []pathEntry\n\tn := t.root\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tidx := inner.findChild(key)\n\t\tpath = append(path, pathEntry{inner, idx})\n\t\tn = inner.children[idx]\n\t}\n\tleaf := n.(*leafNode)\n\n\t// Check if key already exists.\n\tpos, found := leaf.find(key)\n\tif found {\n\t\t*leaf.values[pos] = value\n\t\treturn true\n\t}\n\n\t// Insert new key-value.\n\tleaf.insertAt(pos, key, vp)\n\tt.size++\n\n\t// Increment sizes up the path.\n\tfor i := range path {\n\t\tpath[i].inner.sizes[path[i].childIdx]++\n\t}\n\n\t// Split if leaf overflows.\n\tif len(leaf.keys) \u003e fanout {\n\t\tt.splitLeaf(leaf, pos, path)\n\t}\n\n\treturn false\n}\n\n// splitLeaf splits an overflowed leaf node. Uses 90/10 split for append\n// patterns (insertPos == fanout) and 50/50 otherwise.\nfunc (t *BPTree) splitLeaf(leaf *leafNode, insertPos int, path []pathEntry) {\n\tfanout := t.fanout\n\n\t// 90/10 split for append pattern: new key ended up at position fanout\n\t// (greater than all pre-existing keys).\n\tvar mid int\n\tif insertPos == fanout {\n\t\tmid = fanout - 1\n\t} else {\n\t\tmid = (fanout + 1) / 2\n\t}\n\n\t// Create right leaf with entries [mid, fanout+1).\n\tright := newLeafNode(fanout)\n\tright.keys = append(right.keys, leaf.keys[mid:]...)\n\tright.values = append(right.values, leaf.values[mid:]...)\n\n\t// Truncate left leaf to [0, mid).\n\tfor i := mid; i \u003c len(leaf.keys); i++ {\n\t\tleaf.keys[i] = \"\"\n\t\tleaf.values[i] = nil\n\t}\n\tleaf.keys = leaf.keys[:mid]\n\tleaf.values = leaf.values[:mid]\n\n\t// Promote separator to parent.\n\tsep := right.keys[0]\n\tleftSize := len(leaf.keys)\n\trightSize := len(right.keys)\n\n\tif len(path) == 0 {\n\t\t// Root was the leaf; create a new inner root.\n\t\tnewRoot := newInnerNode(fanout)\n\t\tnewRoot.keys = append(newRoot.keys, sep)\n\t\tnewRoot.children = append(newRoot.children, leaf, right)\n\t\tnewRoot.sizes = append(newRoot.sizes, leftSize, rightSize)\n\t\tt.root = newRoot\n\t\treturn\n\t}\n\n\t// Insert into parent.\n\tpe := path[len(path)-1]\n\tparent := pe.inner\n\tchildIdx := pe.childIdx\n\n\tparent.sizes[childIdx] = leftSize\n\tparent.insertChildAt(childIdx+1, sep, right, rightSize)\n\n\tif len(parent.children) \u003e fanout {\n\t\tt.splitInner(parent, path[:len(path)-1])\n\t}\n}\n\n// splitInner splits an overflowed inner node (always 50/50).\nfunc (t *BPTree) splitInner(inner *innerNode, path []pathEntry) {\n\tfanout := t.fanout\n\tmid := (fanout + 1) / 2\n\n\tpromotedKey := inner.keys[mid-1]\n\n\tright := newInnerNode(fanout)\n\tright.keys = append(right.keys, inner.keys[mid:]...)\n\tright.children = append(right.children, inner.children[mid:]...)\n\tright.sizes = append(right.sizes, inner.sizes[mid:]...)\n\n\tfor i := mid - 1; i \u003c len(inner.keys); i++ {\n\t\tinner.keys[i] = \"\"\n\t}\n\tfor i := mid; i \u003c len(inner.children); i++ {\n\t\tinner.children[i] = nil\n\t}\n\tinner.keys = inner.keys[:mid-1]\n\tinner.children = inner.children[:mid]\n\tinner.sizes = inner.sizes[:mid]\n\n\tif len(path) == 0 {\n\t\tnewRoot := newInnerNode(fanout)\n\t\tnewRoot.keys = append(newRoot.keys, promotedKey)\n\t\tnewRoot.children = append(newRoot.children, inner, right)\n\t\tnewRoot.sizes = append(newRoot.sizes, inner.nodeSize(), right.nodeSize())\n\t\tt.root = newRoot\n\t\treturn\n\t}\n\n\tpe := path[len(path)-1]\n\tparent := pe.inner\n\tchildIdx := pe.childIdx\n\n\tparent.sizes[childIdx] = inner.nodeSize()\n\tparent.insertChildAt(childIdx+1, promotedKey, right, right.nodeSize())\n\n\tif len(parent.children) \u003e fanout {\n\t\tt.splitInner(parent, path[:len(path)-1])\n\t}\n}\n\n//----------------------------------------\n// Remove\n\n// Remove deletes a key. Returns the old value and true if the key was found.\nfunc (t *BPTree) Remove(key string) (value any, removed bool) {\n\tif t.root == nil {\n\t\treturn nil, false\n\t}\n\tfanout := t.fanout\n\n\tvar path []pathEntry\n\tn := t.root\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tidx := inner.findChild(key)\n\t\tpath = append(path, pathEntry{inner, idx})\n\t\tn = inner.children[idx]\n\t}\n\tleaf := n.(*leafNode)\n\n\tpos, found := leaf.find(key)\n\tif !found {\n\t\treturn nil, false\n\t}\n\n\tvar vp *any\n\t_, vp = leaf.removeAt(pos)\n\tvalue = *vp\n\tt.size--\n\n\tfor i := range path {\n\t\tpath[i].inner.sizes[path[i].childIdx]--\n\t}\n\n\t// Handle root leaf.\n\tif len(path) == 0 {\n\t\tif len(leaf.keys) == 0 {\n\t\t\tt.root = nil\n\t\t}\n\t\treturn value, true\n\t}\n\n\t// Check underflow.\n\tminKeys := fanout / 2\n\tif len(leaf.keys) \u003e= minKeys {\n\t\tif pos == 0 {\n\t\t\tt.updateSeparator(path)\n\t\t}\n\t\treturn value, true\n\t}\n\n\t// If the minimum key was removed, fix ancestor separators before rebalancing.\n\tif pos == 0 {\n\t\tt.updateSeparator(path)\n\t}\n\n\t// Rebalance.\n\tt.rebalanceLeaf(leaf, path)\n\treturn value, true\n}\n\n// updateSeparator fixes the parent's separator key if the child's min key changed\n// (e.g., after removing the leftmost entry).\nfunc (t *BPTree) updateSeparator(path []pathEntry) {\n\tfor i := len(path) - 1; i \u003e= 0; i-- {\n\t\tpe := path[i]\n\t\tif pe.childIdx \u003e 0 {\n\t\t\tchild := pe.inner.children[pe.childIdx]\n\t\t\tpe.inner.keys[pe.childIdx-1] = child.minKey()\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// rebalanceLeaf handles a leaf that has underflowed (\u003c fanout/2 entries).\n// Tries redistribute from left, then right, then merges with a sibling.\nfunc (t *BPTree) rebalanceLeaf(leaf *leafNode, path []pathEntry) {\n\tfanout := t.fanout\n\tminKeys := fanout / 2\n\tpe := path[len(path)-1]\n\tparent := pe.inner\n\tchildIdx := pe.childIdx\n\n\t// Try redistribute from left sibling.\n\tif childIdx \u003e 0 {\n\t\tleftSib := parent.children[childIdx-1].(*leafNode)\n\t\tif len(leftSib.keys) \u003e minKeys {\n\t\t\tk, v := leftSib.removeAt(len(leftSib.keys) - 1)\n\t\t\tleaf.insertAt(0, k, v)\n\t\t\tparent.keys[childIdx-1] = leaf.keys[0]\n\t\t\tparent.sizes[childIdx-1] = len(leftSib.keys)\n\t\t\tparent.sizes[childIdx] = len(leaf.keys)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Try redistribute from right sibling.\n\tif childIdx \u003c len(parent.children)-1 {\n\t\trightSib := parent.children[childIdx+1].(*leafNode)\n\t\tif len(rightSib.keys) \u003e minKeys {\n\t\t\tk, v := rightSib.removeAt(0)\n\t\t\tleaf.insertAt(len(leaf.keys), k, v)\n\t\t\tparent.keys[childIdx] = rightSib.keys[0]\n\t\t\tparent.sizes[childIdx] = len(leaf.keys)\n\t\t\tparent.sizes[childIdx+1] = len(rightSib.keys)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Merge.\n\tif childIdx \u003e 0 {\n\t\tleftSib := parent.children[childIdx-1].(*leafNode)\n\t\tmergeLeaves(leftSib, leaf, parent, childIdx)\n\t} else {\n\t\trightSib := parent.children[childIdx+1].(*leafNode)\n\t\tmergeLeaves(leaf, rightSib, parent, childIdx+1)\n\t}\n\n\tt.rebalanceInner(path[:len(path)-1])\n}\n\n// mergeLeaves merges right leaf into left and removes right from parent.\nfunc mergeLeaves(left, right *leafNode, parent *innerNode, rightIdx int) {\n\tleft.keys = append(left.keys, right.keys...)\n\tleft.values = append(left.values, right.values...)\n\tparent.removeChildAt(rightIdx)\n\tparent.sizes[rightIdx-1] = len(left.keys)\n}\n\n// rebalanceInner handles an inner node that has underflowed after a child merge.\nfunc (t *BPTree) rebalanceInner(path []pathEntry) {\n\tif len(path) == 0 {\n\t\troot := t.root.(*innerNode)\n\t\tif len(root.children) == 1 {\n\t\t\tt.root = root.children[0]\n\t\t}\n\t\treturn\n\t}\n\n\tpe := path[len(path)-1]\n\tparent := pe.inner\n\tchildIdx := pe.childIdx\n\tchild := parent.children[childIdx].(*innerNode)\n\tfanout := t.fanout\n\tminChildren := fanout / 2\n\n\tif len(child.children) \u003e= minChildren {\n\t\tif childIdx \u003e 0 {\n\t\t\tparent.keys[childIdx-1] = child.minKey()\n\t\t}\n\t\treturn\n\t}\n\n\t// Try redistribute from left sibling.\n\tif childIdx \u003e 0 {\n\t\tleftSib := parent.children[childIdx-1].(*innerNode)\n\t\tif len(leftSib.children) \u003e minChildren {\n\t\t\tredistributeInnerLeft(parent, childIdx, leftSib, child)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Try redistribute from right sibling.\n\tif childIdx \u003c len(parent.children)-1 {\n\t\trightSib := parent.children[childIdx+1].(*innerNode)\n\t\tif len(rightSib.children) \u003e minChildren {\n\t\t\tredistributeInnerRight(parent, childIdx, child, rightSib)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Merge.\n\tif childIdx \u003e 0 {\n\t\tleftSib := parent.children[childIdx-1].(*innerNode)\n\t\tmergeInner(leftSib, child, parent, childIdx)\n\t} else {\n\t\trightSib := parent.children[childIdx+1].(*innerNode)\n\t\tmergeInner(child, rightSib, parent, childIdx+1)\n\t}\n\n\tgrandPath := path[:len(path)-1]\n\tif len(grandPath) == 0 {\n\t\troot := t.root.(*innerNode)\n\t\tif len(root.children) == 1 {\n\t\t\tt.root = root.children[0]\n\t\t}\n\t\treturn\n\t}\n\n\tgpe := grandPath[len(grandPath)-1]\n\tgparent := gpe.inner\n\tgchildIdx := gpe.childIdx\n\tgchild := gparent.children[gchildIdx].(*innerNode)\n\tminChildren = t.fanout / 2\n\n\tif len(gchild.children) \u003c minChildren {\n\t\tt.rebalanceInner(grandPath)\n\t} else if gchildIdx \u003e 0 {\n\t\tgparent.keys[gchildIdx-1] = gchild.minKey()\n\t}\n}\n\n// redistributeInnerLeft moves the last child from left sibling to the\n// deficient child, pulling down the parent separator and pushing up a new one.\nfunc redistributeInnerLeft(parent *innerNode, childIdx int, leftSib, child *innerNode) {\n\tsep := parent.keys[childIdx-1]\n\tchild.keys = append(child.keys, \"\")\n\tcopy(child.keys[1:], child.keys)\n\tchild.keys[0] = sep\n\n\tlastChild := leftSib.children[len(leftSib.children)-1]\n\tlastSize := leftSib.sizes[len(leftSib.sizes)-1]\n\tchild.children = append(child.children, nil)\n\tcopy(child.children[1:], child.children)\n\tchild.children[0] = lastChild\n\tchild.sizes = append(child.sizes, 0)\n\tcopy(child.sizes[1:], child.sizes)\n\tchild.sizes[0] = lastSize\n\n\tparent.keys[childIdx-1] = leftSib.keys[len(leftSib.keys)-1]\n\n\tleftSib.keys[len(leftSib.keys)-1] = \"\"\n\tleftSib.keys = leftSib.keys[:len(leftSib.keys)-1]\n\tleftSib.children[len(leftSib.children)-1] = nil\n\tleftSib.children = leftSib.children[:len(leftSib.children)-1]\n\tleftSib.sizes = leftSib.sizes[:len(leftSib.sizes)-1]\n\n\tparent.sizes[childIdx-1] = leftSib.nodeSize()\n\tparent.sizes[childIdx] = child.nodeSize()\n}\n\n// redistributeInnerRight moves the first child from right sibling to the\n// deficient child, pulling down the parent separator and pushing up a new one.\nfunc redistributeInnerRight(parent *innerNode, childIdx int, child, rightSib *innerNode) {\n\tsep := parent.keys[childIdx]\n\tchild.keys = append(child.keys, sep)\n\n\tfirstChild := rightSib.children[0]\n\tfirstSize := rightSib.sizes[0]\n\tchild.children = append(child.children, firstChild)\n\tchild.sizes = append(child.sizes, firstSize)\n\n\tparent.keys[childIdx] = rightSib.keys[0]\n\n\tcopy(rightSib.keys, rightSib.keys[1:])\n\trightSib.keys[len(rightSib.keys)-1] = \"\"\n\trightSib.keys = rightSib.keys[:len(rightSib.keys)-1]\n\tcopy(rightSib.children, rightSib.children[1:])\n\trightSib.children[len(rightSib.children)-1] = nil\n\trightSib.children = rightSib.children[:len(rightSib.children)-1]\n\tcopy(rightSib.sizes, rightSib.sizes[1:])\n\trightSib.sizes = rightSib.sizes[:len(rightSib.sizes)-1]\n\n\tparent.sizes[childIdx] = child.nodeSize()\n\tparent.sizes[childIdx+1] = rightSib.nodeSize()\n}\n\n// mergeInner merges right inner node into left, pulling down the parent\n// separator, and removes right from parent.\nfunc mergeInner(left, right *innerNode, parent *innerNode, rightIdx int) {\n\tsep := parent.keys[rightIdx-1]\n\tleft.keys = append(left.keys, sep)\n\tleft.keys = append(left.keys, right.keys...)\n\tleft.children = append(left.children, right.children...)\n\tleft.sizes = append(left.sizes, right.sizes...)\n\tparent.removeChildAt(rightIdx)\n\tparent.sizes[rightIdx-1] = left.nodeSize()\n}\n\n//----------------------------------------\n// Stack-based iteration\n\n// iterStack is a stack of (innerNode, childIdx) for traversal.\n// It is ephemeral — built during iteration, discarded after.\ntype iterStack []pathEntry\n\n// descendLeft descends to the leftmost leaf, pushing inner nodes onto the stack.\nfunc descendLeft(n node, stack *iterStack) *leafNode {\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\t*stack = append(*stack, pathEntry{inner, 0})\n\t\tn = inner.children[0]\n\t}\n\treturn n.(*leafNode)\n}\n\n// descendRight descends to the rightmost leaf, pushing inner nodes onto the stack.\nfunc descendRight(n node, stack *iterStack) *leafNode {\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tlast := len(inner.children) - 1\n\t\t*stack = append(*stack, pathEntry{inner, last})\n\t\tn = inner.children[last]\n\t}\n\treturn n.(*leafNode)\n}\n\n// advanceLeaf moves to the next leaf in ascending order via the stack.\n// Returns nil if there is no next leaf.\nfunc advanceLeaf(stack *iterStack) *leafNode {\n\tfor len(*stack) \u003e 0 {\n\t\ttop := \u0026(*stack)[len(*stack)-1]\n\t\ttop.childIdx++\n\t\tif top.childIdx \u003c len(top.inner.children) {\n\t\t\tn := top.inner.children[top.childIdx]\n\t\t\treturn descendLeft(n, stack)\n\t\t}\n\t\t*stack = (*stack)[:len(*stack)-1]\n\t}\n\treturn nil\n}\n\n// retreatLeaf moves to the previous leaf in descending order via the stack.\n// Returns nil if there is no previous leaf.\nfunc retreatLeaf(stack *iterStack) *leafNode {\n\tfor len(*stack) \u003e 0 {\n\t\ttop := \u0026(*stack)[len(*stack)-1]\n\t\ttop.childIdx--\n\t\tif top.childIdx \u003e= 0 {\n\t\t\tn := top.inner.children[top.childIdx]\n\t\t\treturn descendRight(n, stack)\n\t\t}\n\t\t*stack = (*stack)[:len(*stack)-1]\n\t}\n\treturn nil\n}\n\n//----------------------------------------\n// Iterate / ReverseIterate\n\n// Iterate calls cb for each key-value pair in [start, end) ascending order.\n// Empty start/end means no bound. Returns true if stopped early by cb.\n// The tree must not be modified during iteration (no Set or Remove from the callback).\nfunc (t *BPTree) Iterate(start, end string, cb IterCbFn) bool {\n\tif t.root == nil {\n\t\treturn false\n\t}\n\n\tvar stack iterStack\n\tvar leaf *leafNode\n\tvar pos int\n\n\tif start == \"\" {\n\t\tleaf = descendLeft(t.root, \u0026stack)\n\t\tpos = 0\n\t} else {\n\t\tleaf, pos, stack = t.descendToGE(start)\n\t\tif leaf == nil {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tfor leaf != nil {\n\t\tfor pos \u003c len(leaf.keys) {\n\t\t\tk := leaf.keys[pos]\n\t\t\tif end != \"\" \u0026\u0026 k \u003e= end {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif cb(k, *leaf.values[pos]) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tpos++\n\t\t}\n\t\tleaf = advanceLeaf(\u0026stack)\n\t\tpos = 0\n\t}\n\treturn false\n}\n\n// ReverseIterate calls cb for each key-value pair in [start, end] descending order.\n// Empty start/end means no bound. Returns true if stopped early by cb.\n// The tree must not be modified during iteration (no Set or Remove from the callback).\nfunc (t *BPTree) ReverseIterate(start, end string, cb IterCbFn) bool {\n\tif t.root == nil {\n\t\treturn false\n\t}\n\n\tvar stack iterStack\n\tvar leaf *leafNode\n\tvar pos int\n\n\tif end == \"\" {\n\t\tleaf = descendRight(t.root, \u0026stack)\n\t\tpos = len(leaf.keys) - 1\n\t} else {\n\t\tleaf, pos, stack = t.descendToLE(end)\n\t\tif leaf == nil {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tfor leaf != nil {\n\t\tfor pos \u003e= 0 {\n\t\t\tk := leaf.keys[pos]\n\t\t\tif start != \"\" \u0026\u0026 k \u003c start {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif cb(k, *leaf.values[pos]) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tpos--\n\t\t}\n\t\tleaf = retreatLeaf(\u0026stack)\n\t\tif leaf != nil {\n\t\t\tpos = len(leaf.keys) - 1\n\t\t}\n\t}\n\treturn false\n}\n\n// IterateByOffset calls cb for count entries starting at the offset-th entry\n// in ascending order. Returns true if stopped early by cb.\n// The tree must not be modified during iteration (no Set or Remove from the callback).\nfunc (t *BPTree) IterateByOffset(offset int, count int, cb IterCbFn) bool {\n\tif t.root == nil || offset \u003e= t.size || count \u003c= 0 {\n\t\treturn false\n\t}\n\tif offset \u003c 0 {\n\t\toffset = 0\n\t}\n\n\tleaf, pos, stack := t.descendToOffset(offset)\n\n\tvisited := 0\n\tfor leaf != nil \u0026\u0026 visited \u003c count {\n\t\tfor pos \u003c len(leaf.keys) \u0026\u0026 visited \u003c count {\n\t\t\tif cb(leaf.keys[pos], *leaf.values[pos]) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tvisited++\n\t\t\tpos++\n\t\t}\n\t\tleaf = advanceLeaf(\u0026stack)\n\t\tpos = 0\n\t}\n\treturn false\n}\n\n// ReverseIterateByOffset calls cb for count entries starting at the offset-th\n// entry from the end, in descending order. Returns true if stopped early by cb.\n// The tree must not be modified during iteration (no Set or Remove from the callback).\nfunc (t *BPTree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool {\n\tif t.root == nil || offset \u003e= t.size || count \u003c= 0 {\n\t\treturn false\n\t}\n\tif offset \u003c 0 {\n\t\toffset = 0\n\t}\n\n\tascIdx := t.size - 1 - offset\n\tleaf, pos, stack := t.descendToOffset(ascIdx)\n\n\tvisited := 0\n\tfor leaf != nil \u0026\u0026 visited \u003c count {\n\t\tfor pos \u003e= 0 \u0026\u0026 visited \u003c count {\n\t\t\tif cb(leaf.keys[pos], *leaf.values[pos]) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tvisited++\n\t\t\tpos--\n\t\t}\n\t\tleaf = retreatLeaf(\u0026stack)\n\t\tif leaf != nil {\n\t\t\tpos = len(leaf.keys) - 1\n\t\t}\n\t}\n\treturn false\n}\n\n//----------------------------------------\n// Descent helpers for iteration\n\n// descendToGE descends to the first key \u003e= key, returning the leaf, position, and stack.\nfunc (t *BPTree) descendToGE(key string) (*leafNode, int, iterStack) {\n\tvar stack iterStack\n\tn := t.root\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tidx := inner.findChild(key)\n\t\tstack = append(stack, pathEntry{inner, idx})\n\t\tn = inner.children[idx]\n\t}\n\tleaf := n.(*leafNode)\n\tpos, _ := leaf.find(key)\n\tif pos \u003e= len(leaf.keys) {\n\t\tnext := advanceLeaf(\u0026stack)\n\t\tif next == nil {\n\t\t\treturn nil, 0, nil\n\t\t}\n\t\treturn next, 0, stack\n\t}\n\treturn leaf, pos, stack\n}\n\n// descendToLE descends to the last key \u003c= key, returning the leaf, position, and stack.\nfunc (t *BPTree) descendToLE(key string) (*leafNode, int, iterStack) {\n\tvar stack iterStack\n\tn := t.root\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tidx := inner.findChild(key)\n\t\tstack = append(stack, pathEntry{inner, idx})\n\t\tn = inner.children[idx]\n\t}\n\tleaf := n.(*leafNode)\n\tpos, found := leaf.find(key)\n\tif found {\n\t\treturn leaf, pos, stack\n\t}\n\tif pos \u003e 0 {\n\t\treturn leaf, pos - 1, stack\n\t}\n\tprev := retreatLeaf(\u0026stack)\n\tif prev == nil {\n\t\treturn nil, 0, nil\n\t}\n\treturn prev, len(prev.keys) - 1, stack\n}\n\n// descendToOffset descends to the offset-th entry using sizes[],\n// returning the leaf, position within leaf, and stack.\nfunc (t *BPTree) descendToOffset(offset int) (*leafNode, int, iterStack) {\n\tvar stack iterStack\n\tn := t.root\n\trem := offset\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tfound := false\n\t\tfor i, s := range inner.sizes {\n\t\t\tif rem \u003c s {\n\t\t\t\tstack = append(stack, pathEntry{inner, i})\n\t\t\t\tn = inner.children[i]\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\trem -= s\n\t\t}\n\t\tif !found {\n\t\t\tpanic(\"descendToOffset: offset out of range\")\n\t\t}\n\t}\n\treturn n.(*leafNode), rem, stack\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":"QiUw/bgEz7j1Oy82AsBiyNzTvch2Y2hP2v+ye7Sd+FsC+IES8QIwZrLx7lhjWHncrAD/onfaH/f7TTk5dQeWVw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","package":{"name":"checkpoint","path":"gno.land/p/kourt/checkpoint/v0","files":[{"name":"checkpoint.gno","body":"// Package checkpoint remembers what a number used to be.\n//\n// A Series is one value's history. It answers what that value held during a\n// past epoch, which is the question governance asks: voting weight has to be\n// read as of the moment a question was put, or it can be borrowed for the\n// length of one transaction and voted twice.\n//\n// It was written for a token's balances and its total supply, and there is\n// nothing about it that knows what a token is. Anything a realm wants to look\n// back at — a fee, a quorum, a member count — is a Series.\n//\n// # What it costs\n//\n// One fact about gno shapes the whole thing: storage deposit is charged per\n// OBJECT, on the DELTA of its encoded size (gnovm/pkg/gnolang/store.go,\n// LastObjectSize, at 100ugnot/byte). A field added to a record that was going\n// to be written anyway is paid for once, at creation, and costs nothing on\n// every write after. On the EVM every extra word touched is a fresh SSTORE\n// forever, which is the entire reason OpenZeppelin's ERC20Votes appends to an\n// array per block — and the reason that shape does not have to be inherited.\n//\n// So: two points inline, the rest paged into an archive. The overwhelmingly\n// common query is answered without touching the archive at all, and a value\n// that changes several times within one epoch coalesces into a single update\n// of a record already being written.\n//\n// # Epochs, not blocks\n//\n// The caller supplies the epoch and this package never asks what one is. That\n// is deliberate — the clock is policy. Quantising to something coarser than a\n// block is what makes the cost work, because a checkpoint per block maximises\n// the one thing gno charges dearly for, a brand new key.\n//\n// It also turns the anti-flash-loan property from a convention into an\n// invariant, if the caller refuses to answer for the current epoch: a\n// transaction cannot outlive its block and a block cannot outlive its epoch,\n// so a borrowed balance cannot be voted and returned. This package cannot\n// enforce that, because it does not know what time it is. It is the caller's\n// half of the bargain and worth writing down where the caller will read it.\npackage checkpoint\n\nimport (\n\t\"strings\"\n\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\n// Sep separates a caller's key from this package's own suffix.\n//\n// Exported because a caller sharing one Archive between several keyspaces\n// usually wants the same byte for its own compound keys, and two constants\n// with the same value in different files eventually disagree.\n//\n// A key may not contain it. That is checked rather than documented, because\n// the failure is silent: one key's range scan would reach into another's and\n// return a plausible number belonging to somebody else.\nconst Sep = \"\\x00\"\n\n// MaxKey bounds a key.\n//\n// A key is not stored once. It is the prefix of every page key this series ever\n// writes, so its length multiplies by the number of pages a long-lived value\n// accumulates — and the pages are what the design buys with the deposit it\n// saves elsewhere.\n//\n// Exported so a caller can check before calling rather than discover by\n// panicking. A hundred and twenty-eight is room for a bech32 address twice over\n// with a separator between, which is the largest sensible key the realm this\n// came out of could form.\nconst MaxKey = 128\n\n// PageEpochs is how many epochs one archive page covers.\n//\n// It bounds the archive in both directions, and both matter.\n//\n// Downwards: one new key buys this many checkpoints instead of one, which is\n// what keeps the marginal cost of history near zero.\n//\n// Upwards: a page cannot grow past one bucket. Keeping every checkpoint for a\n// key in a single page would be CHEAPER in keys — one per key, forever — and\n// that is exactly its problem. Appending stays cheap, since the deposit is\n// charged on the size delta, but the whole object is deserialised to answer\n// anything about it. A key with years behind it would pay to load all of them\n// to answer about one epoch.\nconst PageEpochs = uint32(32)\n\n// Archive is the shared store the older points live in.\n//\n// One Archive can hold any number of Series, told apart by key. Bounding each\n// scan to its own key's prefix is what keeps them apart, and it is the reason\n// a key may not contain Sep.\ntype Archive struct {\n\tt *bptree.BPTree\n}\n\n// NewArchive returns an empty archive.\nfunc NewArchive() *Archive { return \u0026Archive{t: bptree.NewBPTree32()} }\n\n// Size is the number of pages stored, across every key.\n//\n// Exported for the caller that wants to assert what its own history costs —\n// the count is the number of KEYS bought, which is the expensive quantity.\nfunc (a *Archive) Size() int { return a.t.Size() }\n\n// page is one bucket of archived checkpoints for one key, ascending.\n//\n// One packed string, not slices: gno stores a []int64 or []uint32 as an array of\n// TypedValues at ~40 bytes an element, but a string (like a []byte) at one byte\n// each. So the entries pack big-endian into a single string — 4 bytes of epoch\n// then 8 of value, 12 bytes per entry — which is ~3x smaller than the two-slice\n// form and is what the archive's locked deposit is charged on. Appending\n// reassigns this *page's field (no tree Set), so a write still dirties exactly one\n// object.\ntype page struct {\n\tpacked string // ascending [epoch:4 BE][value:8 BE] entries, 12 bytes each\n}\n\n// Series is one value's history: the two most recent points inline, everything\n// older in the archive.\n//\n//\tat \u003e= e0        -\u003e cur\n//\te1 \u003c= at \u003c e0   -\u003e prev\n//\tat \u003c e1         -\u003e the archive\n//\n// e1 == 0 means the second slot was never used, and prev is then zero, which\n// is the correct answer for every epoch below e0 — so a key that has held a\n// steady value since it was created never touches the archive at all. That\n// case is the overwhelming majority of queries.\n//\n// That sentinel is why epochs are 1-BASED and SetAt refuses zero. Zero has to\n// mean \"before this series existed\" and cannot also be an epoch somebody wrote\n// in, or the two readings collide: a value written at epoch 0 leaves e1 == 0\n// after the next change, the roll after that reads it as an empty slot, and the\n// point is dropped instead of archived. Silently — the archive stays empty and\n// every query below the newest two points answers zero.\n//\n// The realm this came out of never could have hit it, because its clock is\n// 1-based for exactly this reason. The assumption did not travel with the code,\n// which is the whole hazard of moving code into a package.\n//\n// Hold it as a VALUE field in whatever record the caller already persists.\n// Mutating it then dirties only that record. A pointer would make it a second\n// object, which is a second key, which is the cost this design exists to\n// avoid.\ntype Series struct {\n\tcur  int64\n\tprev int64\n\te0   uint32\n\te1   uint32\n}\n\n// Value is what the series holds now, without consulting anything.\nfunc (s *Series) Value() int64 { return s.cur }\n\n// Prev is the older of the two inline points: its epoch and value. A zero\n// epoch means that slot was never used (the series has at most one point).\n// Callers assembling a full history read the archive, then Prev, then\n// Since/Value — ascending by construction, since points only roll outward.\nfunc (s *Series) Prev() (uint32, int64) { return s.e1, s.prev }\n\n// Since is the epoch the current value has held from.\n//\n// Useful for asserting that something did NOT write — a caller that expects an\n// operation to be free can check this did not move.\nfunc (s *Series) Since() uint32 { return s.e0 }\n\n// SetAt records v from epoch e onward, rolling whatever falls out of the two\n// inline slots into the archive.\n//\n// e must be at least 1, and must not be below the newest epoch already\n// recorded. Callers pass a chain height quantised to an epoch, which only ever\n// increases.\nfunc (s *Series) SetAt(a *Archive, key string, e uint32, v int64) {\n\tmustBeUsable(key)\n\tif e == 0 {\n\t\t// Refused rather than documented, because the damage is silent and\n\t\t// arrives two changes later. See the note on Series: zero is the\n\t\t// sentinel for an unused slot, so a real checkpoint written there is\n\t\t// read as an empty one and dropped on the roll after next.\n\t\tpanic(\"checkpoint: epochs are 1-based; 0 means before the series existed\")\n\t}\n\tif e \u003c s.e0 {\n\t\t// The clock went backwards. Impossible on a chain, so this means the\n\t\t// caller is not on one — a test harness that resets the height between\n\t\t// cases produces exactly this.\n\t\t//\n\t\t// Worth a panic rather than a comment because the failure it prevents\n\t\t// is invisible: a checkpoint written below the newest one pushes a\n\t\t// stale value into the previous slot and moves e0 backwards, and every\n\t\t// later query returns a plausible, wrong number.\n\t\tpanic(\"checkpoint: the clock went backwards\")\n\t}\n\tif e == s.e0 {\n\t\t// The second and later change within one epoch. No new key, and the\n\t\t// object was going to be written anyway: free, in the sense that\n\t\t// matters.\n\t\ts.cur = v\n\t\treturn\n\t}\n\tif s.e1 != 0 {\n\t\ta.append(key, s.e1, s.prev)\n\t}\n\ts.prev, s.e1 = s.cur, s.e0\n\ts.cur, s.e0 = v, e\n}\n\n// ValueAt answers what the series held during epoch at.\n//\n// Zero for an epoch before the series began, which is exact rather than a\n// miss: the key held nothing then.\nfunc (s *Series) ValueAt(a *Archive, key string, at uint32) int64 {\n\tif at \u003e= s.e0 {\n\t\treturn s.cur\n\t}\n\t// Covers e1 == 0 as well, where prev is zero and zero is exact.\n\tif at \u003e= s.e1 {\n\t\treturn s.prev\n\t}\n\tmustBeUsable(key)\n\tout := int64(0)\n\t// Bounded below by this key's own prefix so the descent cannot walk back\n\t// into the previous key's pages. ReverseIterate is [start, end] descending\n\t// with end inclusive (bptree tree.gno, \"ReverseIterate calls cb\"), which is\n\t// the floor query — no binary search of our own.\n\ta.t.ReverseIterate(key+Sep, pageKey(key, at/PageEpochs), func(_ string, pv any) bool {\n\t\ts := pv.(*page).packed\n\t\tfor i := len(s)/12 - 1; i \u003e= 0; i-- {\n\t\t\toff := i * 12\n\t\t\tif rd32(s, off) \u003c= at {\n\t\t\t\tout = rd64(s, off+4)\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\t// Pages bucket EPOCHS, not checkpoints, so the page a query lands in\n\t\t// may hold only points later than the epoch being asked about. Keep\n\t\t// walking down; the answer is in an older page.\n\t\treturn false\n\t})\n\treturn out\n}\n\nfunc (a *Archive) append(key string, e uint32, v int64) {\n\tpk := pageKey(key, e/PageEpochs)\n\tentry := be32(e) + be64(v) // 12 bytes: epoch then value\n\tif pv := a.t.Get(pk); pv != nil {\n\t\tp := pv.(*page)\n\t\t// Reassign the field on the pointer the tree already holds. No Set, so the\n\t\t// leaf and its whole inner path stay clean and the write dirties only this\n\t\t// *page. Entries arrive ascending by construction, so there is nothing to\n\t\t// sort.\n\t\tp.packed += entry\n\t\treturn\n\t}\n\ta.t.Set(pk, \u0026page{packed: entry})\n}\n\n// mustBeUsable refuses a key that would collide with another key's pages.\n//\n// A p/ package's callers are not the people who wrote it, so this is a case\n// that can happen rather than defensive decoration. The cost is a scan of a\n// short key against the tree operations it precedes, which is nothing.\nfunc mustBeUsable(key string) {\n\tif strings.Contains(key, Sep) {\n\t\tpanic(\"checkpoint: a key may not contain checkpoint.Sep\")\n\t}\n\tif len(key) \u003e MaxKey {\n\t\tpanic(\"checkpoint: that key is too long\")\n\t}\n\tif key == \"\" {\n\t\t// An empty key is not wrong so much as unusable: its page range is a\n\t\t// prefix of every other key's, so it would scan into whatever sorts\n\t\t// first. Refused for the same reason as one containing the separator.\n\t\tpanic(\"checkpoint: a key may not be empty\")\n\t}\n}\n\n// be32 is big-endian and fixed width, so byte order is numeric order.\n//\n// The floor query is a range scan over these keys and nothing else puts them\n// in order. Little-endian would sort pages 0, 1 and 2 correctly and come apart\n// at 256.\nfunc be32(u uint32) string {\n\tb := [4]byte{byte(u \u003e\u003e 24), byte(u \u003e\u003e 16), byte(u \u003e\u003e 8), byte(u)}\n\treturn string(b[:])\n}\n\n// be64 packs an int64 value big-endian (via its uint64 bit pattern, so a negative\n// value round-trips through rd64). Together with be32 it forms a page's 12-byte\n// entry.\nfunc be64(v int64) string {\n\tu := uint64(v)\n\tvar b [8]byte\n\tfor i := 7; i \u003e= 0; i-- {\n\t\tb[i] = byte(u \u0026 0xff)\n\t\tu \u003e\u003e= 8\n\t}\n\treturn string(b[:])\n}\n\n// rd32 and rd64 read a packed entry's epoch and value back from a page string.\nfunc rd32(s string, off int) uint32 {\n\treturn uint32(s[off])\u003c\u003c24 | uint32(s[off+1])\u003c\u003c16 | uint32(s[off+2])\u003c\u003c8 | uint32(s[off+3])\n}\n\nfunc rd64(s string, off int) int64 {\n\tvar u uint64\n\tfor i := 0; i \u003c 8; i++ {\n\t\tu = (u \u003c\u003c 8) | uint64(s[off+i])\n\t}\n\treturn int64(u)\n}\n\nfunc pageKey(key string, page uint32) string { return key + Sep + be32(page) }\n\n// Trim deletes archive pages of key that lie entirely below keepFrom — except\n// the newest such page — oldest-first, at most maxPages per call. It returns\n// how many pages were removed.\n//\n// The exception is the floor-preservation invariant: a value set below the\n// horizon and unchanged since is still the correct answer for epochs INSIDE\n// the kept window, and its floor entry lives in the newest all-below page.\n// Sparing that one page keeps ValueAt exact at and above keepFrom through\n// every intermediate state of a bounded, multi-call trim. A page straddling\n// the horizon is never a candidate (it is not entirely below), so up to\n// PageEpochs-1 epochs of over-retention are kept deliberately.\n//\n// Deletion frees the page object's bytes; on chains with storage deposits the\n// locked amount is released by the same mechanism that charged it. Trim never\n// touches the two inline points — recent reads stay exact by construction.\n//\n// The walk is collect-then-remove because the tree forbids mutation from an\n// iteration callback, and it is bounded to maxPages+1 keys of collection, so\n// a call's cost is a constant of the caller's choosing.\nfunc (a *Archive) Trim(key string, keepFrom uint32, maxPages int) int {\n\tmustBeUsable(key)\n\tif maxPages \u003c= 0 {\n\t\treturn 0\n\t}\n\t// Candidates are pages strictly below the horizon's own page: page P\n\t// covers epochs [P*PageEpochs, P*PageEpochs+PageEpochs-1], so P \u003c\n\t// keepFrom/PageEpochs puts every epoch in P below keepFrom. Iterate is\n\t// [start, end) ascending, which is exactly the candidate range.\n\tend := pageKey(key, keepFrom/PageEpochs)\n\tkeys := []string{}\n\ta.t.Iterate(key+Sep, end, func(k string, _ any) bool {\n\t\tkeys = append(keys, k)\n\t\treturn len(keys) \u003e= maxPages+1\n\t})\n\tvar del []string\n\tif len(keys) == maxPages+1 {\n\t\t// The budget's worth, oldest-first. The newest collected candidate —\n\t\t// and anything beyond it the bounded walk never reached — survives,\n\t\t// so the newest all-below page survives.\n\t\tdel = keys[:maxPages]\n\t} else if len(keys) \u003e 0 {\n\t\t// The walk exhausted the candidates: the last one collected IS the\n\t\t// newest all-below page. Spare it.\n\t\tdel = keys[:len(keys)-1]\n\t}\n\tfor _, k := range del {\n\t\ta.t.Remove(k)\n\t}\n\treturn len(del)\n}\n\n// WalkDesc visits key's ARCHIVED checkpoints newest-first, stopping when fn\n// returns true. The two inline points are the Series' own fields and are not\n// visited — a caller assembling a history appends them itself (they are always\n// newer than anything archived, because points only ever roll OUT of the\n// inline slots). Bounded by the key's page prefix like every other walk here.\nfunc (a *Archive) WalkDesc(key string, fn func(e uint32, v int64) bool) {\n\tmustBeUsable(key)\n\ta.t.ReverseIterate(key+Sep, pageKey(key, ^uint32(0)), func(_ string, pv any) bool {\n\t\ts := pv.(*page).packed\n\t\tfor i := len(s)/12 - 1; i \u003e= 0; i-- {\n\t\t\toff := i * 12\n\t\t\tif fn(rd32(s, off), rd64(s, off+4)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/kourt/checkpoint/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":"ctXkaNiKM5i2uCOQFk3eyn4/v1+/vSSCzt5TYQ7f314AVBYSyQow75tC+aJHA8avvJRNkavuWYt6J5rVAp+pSA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","package":{"name":"curve","path":"gno.land/p/kourt/curve/v0","files":[{"name":"curve.gno","body":"// Package curve is a linear, one-way bonding curve for issuing a token against a\n// reserve: price rises linearly with the position on the curve, and the cost of\n// minting is the exact integral, computed in 128-bit so it never overflows and\n// never issues a coin for less than its backing.\n//\n// # Why a reciprocal slope\n//\n// The marginal price is p(s) = s/D — a RECIPROCAL denominator D, not a numerator\n// slope. That is not cosmetic. The cost of moving from s0 to s1 is the integral\n// (s1²−s0²)/(2D). With a numerator slope k the product k·(s1²−s0²) can exceed\n// 2^128 for a realistic k at a large supply, and the single 128-bit multiply\n// overflows. As 1/D the numerator is only s1²−s0², at most cap² which for a cap\n// near 9.2e14 is about 2^100 — always inside 128 bits — and D sits safely in the\n// divisor. D is chosen from economics: larger D is a gentler curve.\n//\n// # No coin is ever minted below its backing\n//\n// Two rounding rules, and one belt-and-suspenders check, guarantee it:\n//\n//   - Cost rounds UP. The buyer pays at least the integral, so backing (the sum\n//     of integrals) never exceeds treasury (the sum charged).\n//   - Minted rounds DOWN. It floors the number of coins a payment buys — but it\n//     does not trust the square root. It uses isqrt only for a CANDIDATE, then\n//     corrects ±1 against the canonical Cost, so an off-by-one in the root can\n//     never over-issue. This re-check is mandatory, not optional.\n//\n// # A value, not an object\n//\n// A Curve is immutable configuration (the slope denominator and the position\n// cap); it holds no mutable state and is never a heap object. The CURVE POSITION\n// — how far up the curve issuance has walked — lives in the consuming realm as a\n// monotonic counter it passes in as `from`. Burning or redeeming the token must\n// NOT move that counter back: the curve prices the next mint off total-ever-\n// minted, and walking it backward would let the same region be bought twice.\npackage curve\n\nimport (\n\t\"math/bits\"\n\t\"math/overflow\"\n)\n\nconst maxInt64 = int64(9223372036854775807)\n\n// maxCap bounds the position cap so cap² ≤ 2^100 and every 128-bit operand here\n// stays well inside 2^126 (where isqrt128's search bound is exact). 2^50 ≈ 1.1e15\n// comfortably covers a token's ~9.2e14 supply ceiling.\nconst maxCap = int64(1) \u003c\u003c 50\n\n// Curve is a linear one-way bonding curve. Build it with New.\ntype Curve struct {\n\td   int64 // reciprocal slope: marginal price p(s) = s/d\n\tcap int64 // the highest position issuance may reach\n}\n\n// New builds a curve with reciprocal slope d and position cap. A larger d is a\n// gentler price rise. cap bounds the position so cap² stays inside 128 bits and\n// the token's own supply ceiling is respected.\nfunc New(d, cap int64) Curve {\n\tif d \u003c 1 || d \u003e maxInt64/2 {\n\t\tpanic(\"curve: d must be in [1, MaxInt64/2] so 2d does not overflow\")\n\t}\n\tif cap \u003c 1 || cap \u003e maxCap {\n\t\tpanic(\"curve: cap must be in [1, 2^50]\")\n\t}\n\treturn Curve{d: d, cap: cap}\n}\n\n// D and Cap expose the construction parameters.\nfunc (c Curve) D() int64   { return c.d }\nfunc (c Curve) Cap() int64 { return c.cap }\n\n// Cost is the coin a buyer must pay to move the position from `from` to\n// `from+delta`, the integral of the price over that span, ROUNDED UP. ok is false\n// when the move would pass the cap or the cost would not fit in an int64 (only at\n// absurd positions). Minting zero costs zero.\nfunc (c Curve) Cost(from, delta int64) (coin int64, ok bool) {\n\tif from \u003c 0 || delta \u003c 0 || from \u003e c.cap {\n\t\treturn 0, false\n\t}\n\ts1, add := overflow.Add64(from, delta)\n\tif !add || s1 \u003e c.cap {\n\t\treturn 0, false\n\t}\n\t// diff = s1² − s0², an exact 128-bit value (s1 ≥ s0 so it never borrows past 0).\n\tsh1, sl1 := bits.Mul64(uint64(s1), uint64(s1))\n\tsh0, sl0 := bits.Mul64(uint64(from), uint64(from))\n\tlo, borrow := bits.Sub64(sl1, sl0, 0)\n\thi, _ := bits.Sub64(sh1, sh0, borrow)\n\n\tm := uint64(2 * c.d)\n\t// Ceil-divide the 128-bit diff by m: (diff + m − 1) / m.\n\tlo2, carry := bits.Add64(lo, m-1, 0)\n\thi2 := hi + carry\n\tif hi2 \u003e= m {\n\t\t// The quotient would not fit int64 (and bits.Div64 REQUIRES hi \u003c m). This IS\n\t\t// reachable well below the position cap — for the court's d=1e9 it fires around\n\t\t// s1≈1.9e14, far under cap≈9.2e14 — but only at positions whose fill cost exceeds\n\t\t// MaxInt64 µGNOT, i.e. more GNOT than can exist, so no funded Buy reaches it.\n\t\t// Do NOT delete this as \"dead\": Minted relies on ok=false meaning \"unaffordable\",\n\t\t// and without the guard bits.Div64 would violate its hi\u003cm precondition (over-issue).\n\t\treturn 0, false\n\t}\n\tq, _ := bits.Div64(hi2, lo2, m)\n\tif q \u003e uint64(maxInt64) {\n\t\t// Same story one step later: the cost is a valid 128-bit value but exceeds int64,\n\t\t// so it is unaffordable (MaxInt64 µGNOT is more than the whole supply's worth).\n\t\treturn 0, false\n\t}\n\treturn int64(q), true\n}\n\n// Minted is the largest whole number of coins `coin` can buy starting at position\n// `from`, and the coin actually spent on them (≤ coin; the caller keeps or refunds\n// the remainder). It floors: it finds a candidate with a 128-bit integer square\n// root, then corrects ±1 against the canonical Cost, so issued coin is never worth\n// more than what was paid, and it is zero when even one coin costs more than\n// `coin`.\nfunc (c Curve) Minted(from, coin int64) (delta, spent int64) {\n\tif from \u003c 0 || from \u003e= c.cap || coin \u003c= 0 {\n\t\treturn 0, 0\n\t}\n\t// Short-circuit a payment large enough to buy the whole remaining curve. This\n\t// bounds the isqrt operand below to at most cap² (so isqrt128's search bound is\n\t// exact) and handles the huge-coin case explicitly instead of relying on the\n\t// later cap clamp. When filling to the cap costs more than an int64 can hold\n\t// (a very steep curve), full is !ok and coin (≤ MaxInt64) is necessarily below\n\t// it, so we fall through — and then s1 \u003c cap keeps the operand under cap² too.\n\tif full, ok := c.Cost(from, c.cap-from); ok \u0026\u0026 coin \u003e= full {\n\t\treturn c.cap - from, full\n\t}\n\t// operand = from² + 2·d·coin, a 128-bit value; s1 = floor(sqrt(operand)).\n\tfh, fl := bits.Mul64(uint64(from), uint64(from))\n\tph, pl := bits.Mul64(uint64(2*c.d), uint64(coin))\n\tlo, carry := bits.Add64(fl, pl, 0)\n\thi := fh + ph + carry\n\tr := isqrt128(hi, lo)\n\n\t// UNREACHABLE AND LOAD-BEARING, which is not a contradiction — the same standing\n\t// as the hi2 \u003e= m guard above, and it is worth being explicit because no test can\n\t// pin it. Nothing reaches it while the domain check at the top of this function\n\t// stands: on fall-through coin \u003c full, so the operand is at most cap²−1 and r is\n\t// at most cap−1. There is NO margin in that bound (r does reach exactly cap−1), and\n\t// the clamp is the second line, not a redundant one. Delete BOTH and, on a curve\n\t// New will happily build, Minted(1, -1) returns 9223372036854775807 units for a\n\t// spend of 0 — measured, not argued: uint64(-1) makes the operand exceed isqrt128's\n\t// exact range, r comes back as its search bound 2^63, int64(r) is MinInt64, and\n\t// s1 - from WRAPS to MaxInt64, which the delta \u003c= 0 exit below then waves through\n\t// as a positive mint.\n\tvar s1 int64\n\tif r \u003e uint64(c.cap) {\n\t\ts1 = c.cap // clamp before the correction so Cost never sees an over-cap span\n\t} else {\n\t\ts1 = int64(r)\n\t}\n\n\t// Step DOWN while the rounded-up cost of reaching s1 exceeds the payment.\n\tfor s1 \u003e from {\n\t\tcst, ok := c.Cost(from, s1-from)\n\t\tif ok \u0026\u0026 cst \u003c= coin {\n\t\t\tbreak\n\t\t}\n\t\ts1--\n\t}\n\t// Step UP while one more coin still fits (and stays under the cap).\n\tfor s1 \u003c c.cap {\n\t\tcst, ok := c.Cost(from, s1+1-from)\n\t\tif !ok || cst \u003e coin {\n\t\t\tbreak\n\t\t}\n\t\ts1++\n\t}\n\tdelta = s1 - from\n\tif delta \u003c= 0 {\n\t\treturn 0, 0\n\t}\n\tspent, _ = c.Cost(from, delta) // round-up cost of exactly what was minted\n\treturn delta, spent\n}\n\n// Price is the marginal price at position s (coin per unit), floor(s/d). Backing\n// is the reserve behind one unit at position s, exactly half the marginal price —\n// the reason every buyer pays about twice the backing of the coin they buy.\nfunc (c Curve) Price(s int64) int64   { return s / c.d }\nfunc (c Curve) Backing(s int64) int64 { return s / (2 * c.d) }\n\n// isqrt128 returns floor(sqrt(x)) for the 128-bit unsigned value x = hi:lo, by\n// binary search on the ≤64-bit result — each step squares a candidate with a\n// 128-bit multiply and compares. Division-free and deterministic.\nfunc isqrt128(hi, lo uint64) uint64 {\n\tvar ans, lo_ uint64\n\thi_ := uint64(1) \u003c\u003c 63 // search bound: covers every result up to sqrt(2^126)=2^63 (our operands stay \u003c 2^100)\n\tfor lo_ \u003c= hi_ {\n\t\tmid := lo_ + (hi_-lo_)/2\n\t\tmh, ml := bits.Mul64(mid, mid)\n\t\tif mh \u003c hi || (mh == hi \u0026\u0026 ml \u003c= lo) { // mid² ≤ x\n\t\t\tans = mid\n\t\t\tif mid == ^uint64(0) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tlo_ = mid + 1\n\t\t} else {\n\t\t\tif mid == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\thi_ = mid - 1\n\t\t}\n\t}\n\treturn ans\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/kourt/curve/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":"IxNZovRcrW8rX8BQsyTYvlGRNy6fdmJyXqfrjs89jLZ2xwvJzgGzfcG0sYCHtDrYbHDkVml++qwWRZxweL0qUQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","package":{"name":"cford32","path":"gno.land/p/nt/cford32/v0","files":[{"name":"cford32.gno","body":"// Modified from the Go Source code for encoding/base32.\n// Copyright 2009 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package cford32 implements a base32-like encoding/decoding package, with the\n// encoding scheme [specified by Douglas Crockford].\n//\n// From the website, the requirements of said encoding scheme are to:\n//\n//   - Be human readable and machine readable.\n//   - Be compact. Humans have difficulty in manipulating long strings of arbitrary symbols.\n//   - Be error resistant. Entering the symbols must not require keyboarding gymnastics.\n//   - Be pronounceable. Humans should be able to accurately transmit the symbols to other humans using a telephone.\n//\n// This is slightly different from a simple difference in encoding table from\n// the Go's stdlib `encoding/base32`, as when decoding the characters i I l L are\n// parsed as 1, and o O is parsed as 0.\n//\n// This package additionally provides ways to encode uint64's efficiently,\n// as well as efficient encoding to a lowercase variation of the encoding.\n// The encodings never use paddings.\n//\n// # Uint64 Encoding\n//\n// Aside from lower/uppercase encoding, there is a compact encoding, allowing\n// to encode all values in [0,2^34), and the full encoding, allowing all\n// values in [0,2^64). The compact encoding uses 7 characters, and the full\n// encoding uses 13 characters. Both are parsed unambiguously by the Uint64\n// decoder.\n//\n// The compact encodings have the first character between ['0','f'], while the\n// full encoding's first character ranges between ['g','z']. Practically, in\n// your usage of the package, you should consider which one to use and stick\n// with it, while considering that the compact encoding, once it reaches 2^34,\n// automatically switches to the full encoding. The properties of the generated\n// strings are still maintained: for instance, any two encoded uint64s x,y\n// consistently generated with the compact encoding, if the numeric value is\n// x \u003c y, will also be x \u003c y in lexical ordering. However, values [0,2^34) have a\n// \"double encoding\", which if mixed together lose the lexical ordering property.\n//\n// The Uint64 encoding is most useful for generating string versions of Uint64\n// IDs. Practically, it allows you to retain sleek and compact IDs for your\n// application for the first 2^34 (\u003e17 billion) entities, while seamlessly\n// rolling over to the full encoding should you exceed that. You are encouraged\n// to use it unless you have a requirement or preferences for IDs consistently\n// being always the same size.\n//\n// To use the cford32 encoding for IDs, you may want to consider using package\n// [gno.land/p/nt/seqid/v0].\n//\n// [specified by Douglas Crockford]: https://www.crockford.com/base32.html\npackage cford32\n\nimport (\n\t\"io\"\n\t\"strconv\"\n)\n\nconst (\n\tencTable      = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\"\n\tencTableLower = \"0123456789abcdefghjkmnpqrstvwxyz\"\n\n\t// each line is 16 bytes\n\tdecTable = \"\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 00-0f\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 10-1f\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 20-2f\n\t\t\"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\xff\\xff\\xff\\xff\\xff\\xff\" + // 30-3f\n\t\t\"\\xff\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f\\x10\\x11\\x01\\x12\\x13\\x01\\x14\\x15\\x00\" + // 40-4f\n\t\t\"\\x16\\x17\\x18\\x19\\x1a\\xff\\x1b\\x1c\\x1d\\x1e\\x1f\\xff\\xff\\xff\\xff\\xff\" + // 50-5f\n\t\t\"\\xff\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f\\x10\\x11\\x01\\x12\\x13\\x01\\x14\\x15\\x00\" + // 60-6f\n\t\t\"\\x16\\x17\\x18\\x19\\x1a\\xff\\x1b\\x1c\\x1d\\x1e\\x1f\\xff\\xff\\xff\\xff\\xff\" + // 70-7f\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 80-ff (not ASCII)\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\"\n)\n\n// CorruptInputError is returned by parsing functions when an invalid character\n// in the input is found. The integer value represents the byte index where\n// the error occurred.\n//\n// This is typically because the given character does not exist in the encoding.\ntype CorruptInputError int64\n\nfunc (e CorruptInputError) Error() string {\n\treturn \"illegal cford32 data at input byte \" + strconv.FormatInt(int64(e), 10)\n}\n\n// Uint64 parses a cford32-encoded byte slice into a uint64.\n//\n//   - The parser requires all provided character to be valid cford32 characters.\n//   - The parser disregards case.\n//   - If the first character is '0' \u003c= c \u003c= 'f', then the passed value is assumed\n//     encoded in the compact encoding, and must be 7 characters long.\n//   - If the first character is 'g' \u003c= c \u003c= 'z',  then the passed value is\n//     assumed encoded in the full encoding, and must be 13 characters long.\n//\n// If any of these requirements fail, a CorruptInputError will be returned.\nfunc Uint64(b []byte) (uint64, error) {\n\tif len(b) == 0 {\n\t\treturn 0, CorruptInputError(0)\n\t}\n\tb0 := decTable[b[0]]\n\tswitch {\n\tdefault:\n\t\treturn 0, CorruptInputError(0)\n\tcase len(b) == 7 \u0026\u0026 b0 \u003c 16:\n\t\tdecVals := [7]byte{\n\t\t\tdecTable[b[0]],\n\t\t\tdecTable[b[1]],\n\t\t\tdecTable[b[2]],\n\t\t\tdecTable[b[3]],\n\t\t\tdecTable[b[4]],\n\t\t\tdecTable[b[5]],\n\t\t\tdecTable[b[6]],\n\t\t}\n\t\tfor idx, v := range decVals {\n\t\t\tif v \u003e= 32 {\n\t\t\t\treturn 0, CorruptInputError(idx)\n\t\t\t}\n\t\t}\n\n\t\treturn 0 +\n\t\t\tuint64(decVals[0])\u003c\u003c30 |\n\t\t\tuint64(decVals[1])\u003c\u003c25 |\n\t\t\tuint64(decVals[2])\u003c\u003c20 |\n\t\t\tuint64(decVals[3])\u003c\u003c15 |\n\t\t\tuint64(decVals[4])\u003c\u003c10 |\n\t\t\tuint64(decVals[5])\u003c\u003c5 |\n\t\t\tuint64(decVals[6]), nil\n\tcase len(b) == 13 \u0026\u0026 b0 \u003e= 16 \u0026\u0026 b0 \u003c 32:\n\t\tdecVals := [13]byte{\n\t\t\tdecTable[b[0]] \u0026 0x0F, // disregard high bit\n\t\t\tdecTable[b[1]],\n\t\t\tdecTable[b[2]],\n\t\t\tdecTable[b[3]],\n\t\t\tdecTable[b[4]],\n\t\t\tdecTable[b[5]],\n\t\t\tdecTable[b[6]],\n\t\t\tdecTable[b[7]],\n\t\t\tdecTable[b[8]],\n\t\t\tdecTable[b[9]],\n\t\t\tdecTable[b[10]],\n\t\t\tdecTable[b[11]],\n\t\t\tdecTable[b[12]],\n\t\t}\n\t\tfor idx, v := range decVals {\n\t\t\tif v \u003e= 32 {\n\t\t\t\treturn 0, CorruptInputError(idx)\n\t\t\t}\n\t\t}\n\n\t\treturn 0 +\n\t\t\tuint64(decVals[0])\u003c\u003c60 |\n\t\t\tuint64(decVals[1])\u003c\u003c55 |\n\t\t\tuint64(decVals[2])\u003c\u003c50 |\n\t\t\tuint64(decVals[3])\u003c\u003c45 |\n\t\t\tuint64(decVals[4])\u003c\u003c40 |\n\t\t\tuint64(decVals[5])\u003c\u003c35 |\n\t\t\tuint64(decVals[6])\u003c\u003c30 |\n\t\t\tuint64(decVals[7])\u003c\u003c25 |\n\t\t\tuint64(decVals[8])\u003c\u003c20 |\n\t\t\tuint64(decVals[9])\u003c\u003c15 |\n\t\t\tuint64(decVals[10])\u003c\u003c10 |\n\t\t\tuint64(decVals[11])\u003c\u003c5 |\n\t\t\tuint64(decVals[12]), nil\n\t}\n}\n\nconst mask = 31\n\n// PutUint64 returns a cford32-encoded byte slice.\nfunc PutUint64(id uint64) [13]byte {\n\treturn [13]byte{\n\t\tencTable[id\u003e\u003e60\u0026mask|0x10], // specify full encoding\n\t\tencTable[id\u003e\u003e55\u0026mask],\n\t\tencTable[id\u003e\u003e50\u0026mask],\n\t\tencTable[id\u003e\u003e45\u0026mask],\n\t\tencTable[id\u003e\u003e40\u0026mask],\n\t\tencTable[id\u003e\u003e35\u0026mask],\n\t\tencTable[id\u003e\u003e30\u0026mask],\n\t\tencTable[id\u003e\u003e25\u0026mask],\n\t\tencTable[id\u003e\u003e20\u0026mask],\n\t\tencTable[id\u003e\u003e15\u0026mask],\n\t\tencTable[id\u003e\u003e10\u0026mask],\n\t\tencTable[id\u003e\u003e5\u0026mask],\n\t\tencTable[id\u0026mask],\n\t}\n}\n\n// PutUint64Lower returns a cford32-encoded byte array, swapping uppercase\n// letters with lowercase.\n//\n// For more information on how the value is encoded, see [Uint64].\nfunc PutUint64Lower(id uint64) [13]byte {\n\treturn [13]byte{\n\t\tencTableLower[id\u003e\u003e60\u0026mask|0x10],\n\t\tencTableLower[id\u003e\u003e55\u0026mask],\n\t\tencTableLower[id\u003e\u003e50\u0026mask],\n\t\tencTableLower[id\u003e\u003e45\u0026mask],\n\t\tencTableLower[id\u003e\u003e40\u0026mask],\n\t\tencTableLower[id\u003e\u003e35\u0026mask],\n\t\tencTableLower[id\u003e\u003e30\u0026mask],\n\t\tencTableLower[id\u003e\u003e25\u0026mask],\n\t\tencTableLower[id\u003e\u003e20\u0026mask],\n\t\tencTableLower[id\u003e\u003e15\u0026mask],\n\t\tencTableLower[id\u003e\u003e10\u0026mask],\n\t\tencTableLower[id\u003e\u003e5\u0026mask],\n\t\tencTableLower[id\u0026mask],\n\t}\n}\n\n// PutCompact returns a cford32-encoded byte slice, using the compact\n// representation of cford32 described in the package documentation where\n// possible (all values of id \u003c 1\u003c\u003c34). The lowercase encoding is used.\n//\n// The resulting byte slice will be 7 bytes long for all compact values,\n// and 13 bytes long for\nfunc PutCompact(id uint64) []byte {\n\treturn AppendCompact(id, nil)\n}\n\n// AppendCompact works like [PutCompact] but appends to the given byte slice\n// instead of allocating one anew.\nfunc AppendCompact(id uint64, b []byte) []byte {\n\tconst maxCompact = 1 \u003c\u003c 34\n\tif id \u003c maxCompact {\n\t\treturn append(b,\n\t\t\tencTableLower[id\u003e\u003e30\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e25\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e20\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e15\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e10\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e5\u0026mask],\n\t\t\tencTableLower[id\u0026mask],\n\t\t)\n\t}\n\treturn append(b,\n\t\tencTableLower[id\u003e\u003e60\u0026mask|0x10],\n\t\tencTableLower[id\u003e\u003e55\u0026mask],\n\t\tencTableLower[id\u003e\u003e50\u0026mask],\n\t\tencTableLower[id\u003e\u003e45\u0026mask],\n\t\tencTableLower[id\u003e\u003e40\u0026mask],\n\t\tencTableLower[id\u003e\u003e35\u0026mask],\n\t\tencTableLower[id\u003e\u003e30\u0026mask],\n\t\tencTableLower[id\u003e\u003e25\u0026mask],\n\t\tencTableLower[id\u003e\u003e20\u0026mask],\n\t\tencTableLower[id\u003e\u003e15\u0026mask],\n\t\tencTableLower[id\u003e\u003e10\u0026mask],\n\t\tencTableLower[id\u003e\u003e5\u0026mask],\n\t\tencTableLower[id\u0026mask],\n\t)\n}\n\nfunc DecodedLen(n int) int {\n\treturn n/8*5 + n%8*5/8\n}\n\nfunc EncodedLen(n int) int {\n\treturn n/5*8 + (n%5*8+4)/5\n}\n\n// Encode encodes src using the encoding enc,\n// writing [EncodedLen](len(src)) bytes to dst.\n//\n// The encoding does not contain any padding, unlike Go's base32.\nfunc Encode(dst, src []byte) {\n\t// Copied from encoding/base32/base32.go (go1.22)\n\tif len(src) == 0 {\n\t\treturn\n\t}\n\n\tdi, si := 0, 0\n\tn := (len(src) / 5) * 5\n\tfor si \u003c n {\n\t\t// Combining two 32 bit loads allows the same code to be used\n\t\t// for 32 and 64 bit platforms.\n\t\thi := uint32(src[si+0])\u003c\u003c24 | uint32(src[si+1])\u003c\u003c16 | uint32(src[si+2])\u003c\u003c8 | uint32(src[si+3])\n\t\tlo := hi\u003c\u003c8 | uint32(src[si+4])\n\n\t\tdst[di+0] = encTable[(hi\u003e\u003e27)\u00260x1F]\n\t\tdst[di+1] = encTable[(hi\u003e\u003e22)\u00260x1F]\n\t\tdst[di+2] = encTable[(hi\u003e\u003e17)\u00260x1F]\n\t\tdst[di+3] = encTable[(hi\u003e\u003e12)\u00260x1F]\n\t\tdst[di+4] = encTable[(hi\u003e\u003e7)\u00260x1F]\n\t\tdst[di+5] = encTable[(hi\u003e\u003e2)\u00260x1F]\n\t\tdst[di+6] = encTable[(lo\u003e\u003e5)\u00260x1F]\n\t\tdst[di+7] = encTable[(lo)\u00260x1F]\n\n\t\tsi += 5\n\t\tdi += 8\n\t}\n\n\t// Add the remaining small block\n\tremain := len(src) - si\n\tif remain == 0 {\n\t\treturn\n\t}\n\n\t// Encode the remaining bytes in reverse order.\n\tval := uint32(0)\n\tswitch remain {\n\tcase 4:\n\t\tval |= uint32(src[si+3])\n\t\tdst[di+6] = encTable[val\u003c\u003c3\u00260x1F]\n\t\tdst[di+5] = encTable[val\u003e\u003e2\u00260x1F]\n\t\tfallthrough\n\tcase 3:\n\t\tval |= uint32(src[si+2]) \u003c\u003c 8\n\t\tdst[di+4] = encTable[val\u003e\u003e7\u00260x1F]\n\t\tfallthrough\n\tcase 2:\n\t\tval |= uint32(src[si+1]) \u003c\u003c 16\n\t\tdst[di+3] = encTable[val\u003e\u003e12\u00260x1F]\n\t\tdst[di+2] = encTable[val\u003e\u003e17\u00260x1F]\n\t\tfallthrough\n\tcase 1:\n\t\tval |= uint32(src[si+0]) \u003c\u003c 24\n\t\tdst[di+1] = encTable[val\u003e\u003e22\u00260x1F]\n\t\tdst[di+0] = encTable[val\u003e\u003e27\u00260x1F]\n\t}\n}\n\n// EncodeLower is like [Encode], but uses the lowercase\nfunc EncodeLower(dst, src []byte) {\n\t// Copied from encoding/base32/base32.go (go1.22)\n\tif len(src) == 0 {\n\t\treturn\n\t}\n\n\tdi, si := 0, 0\n\tn := (len(src) / 5) * 5\n\tfor si \u003c n {\n\t\t// Combining two 32 bit loads allows the same code to be used\n\t\t// for 32 and 64 bit platforms.\n\t\thi := uint32(src[si+0])\u003c\u003c24 | uint32(src[si+1])\u003c\u003c16 | uint32(src[si+2])\u003c\u003c8 | uint32(src[si+3])\n\t\tlo := hi\u003c\u003c8 | uint32(src[si+4])\n\n\t\tdst[di+0] = encTableLower[(hi\u003e\u003e27)\u00260x1F]\n\t\tdst[di+1] = encTableLower[(hi\u003e\u003e22)\u00260x1F]\n\t\tdst[di+2] = encTableLower[(hi\u003e\u003e17)\u00260x1F]\n\t\tdst[di+3] = encTableLower[(hi\u003e\u003e12)\u00260x1F]\n\t\tdst[di+4] = encTableLower[(hi\u003e\u003e7)\u00260x1F]\n\t\tdst[di+5] = encTableLower[(hi\u003e\u003e2)\u00260x1F]\n\t\tdst[di+6] = encTableLower[(lo\u003e\u003e5)\u00260x1F]\n\t\tdst[di+7] = encTableLower[(lo)\u00260x1F]\n\n\t\tsi += 5\n\t\tdi += 8\n\t}\n\n\t// Add the remaining small block\n\tremain := len(src) - si\n\tif remain == 0 {\n\t\treturn\n\t}\n\n\t// Encode the remaining bytes in reverse order.\n\tval := uint32(0)\n\tswitch remain {\n\tcase 4:\n\t\tval |= uint32(src[si+3])\n\t\tdst[di+6] = encTableLower[val\u003c\u003c3\u00260x1F]\n\t\tdst[di+5] = encTableLower[val\u003e\u003e2\u00260x1F]\n\t\tfallthrough\n\tcase 3:\n\t\tval |= uint32(src[si+2]) \u003c\u003c 8\n\t\tdst[di+4] = encTableLower[val\u003e\u003e7\u00260x1F]\n\t\tfallthrough\n\tcase 2:\n\t\tval |= uint32(src[si+1]) \u003c\u003c 16\n\t\tdst[di+3] = encTableLower[val\u003e\u003e12\u00260x1F]\n\t\tdst[di+2] = encTableLower[val\u003e\u003e17\u00260x1F]\n\t\tfallthrough\n\tcase 1:\n\t\tval |= uint32(src[si+0]) \u003c\u003c 24\n\t\tdst[di+1] = encTableLower[val\u003e\u003e22\u00260x1F]\n\t\tdst[di+0] = encTableLower[val\u003e\u003e27\u00260x1F]\n\t}\n}\n\n// AppendEncode appends the cford32 encoded src to dst\n// and returns the extended buffer.\nfunc AppendEncode(dst, src []byte) []byte {\n\tn := EncodedLen(len(src))\n\tdst = grow(dst, n)\n\tEncode(dst[len(dst):][:n], src)\n\treturn dst[:len(dst)+n]\n}\n\n// AppendEncodeLower appends the lowercase cford32 encoded src to dst\n// and returns the extended buffer.\nfunc AppendEncodeLower(dst, src []byte) []byte {\n\tn := EncodedLen(len(src))\n\tdst = grow(dst, n)\n\tEncodeLower(dst[len(dst):][:n], src)\n\treturn dst[:len(dst)+n]\n}\n\nfunc grow(s []byte, n int) []byte {\n\t// slices.Grow\n\tif n -= cap(s) - len(s); n \u003e 0 {\n\t\tnews := make([]byte, cap(s)+n)\n\t\tcopy(news[:cap(s)], s[:cap(s)])\n\t\treturn news[:len(s)]\n\t}\n\treturn s\n}\n\n// EncodeToString returns the cford32 encoding of src.\nfunc EncodeToString(src []byte) string {\n\tbuf := make([]byte, EncodedLen(len(src)))\n\tEncode(buf, src)\n\treturn string(buf)\n}\n\n// EncodeToStringLower returns the cford32 lowercase encoding of src.\nfunc EncodeToStringLower(src []byte) string {\n\tbuf := make([]byte, EncodedLen(len(src)))\n\tEncodeLower(buf, src)\n\treturn string(buf)\n}\n\nfunc decode(dst, src []byte) (n int, err error) {\n\tdsti := 0\n\tolen := len(src)\n\n\tfor len(src) \u003e 0 {\n\t\t// Decode quantum using the base32 alphabet\n\t\tvar dbuf [8]byte\n\t\tdlen := 8\n\n\t\tfor j := 0; j \u003c 8; {\n\t\t\tif len(src) == 0 {\n\t\t\t\t// We have reached the end and are not expecting any padding\n\t\t\t\tdlen = j\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tin := src[0]\n\t\t\tsrc = src[1:]\n\t\t\tdbuf[j] = decTable[in]\n\t\t\tif dbuf[j] == 0xFF {\n\t\t\t\treturn n, CorruptInputError(olen - len(src) - 1)\n\t\t\t}\n\t\t\tj++\n\t\t}\n\n\t\t// Pack 8x 5-bit source blocks into 5 byte destination\n\t\t// quantum\n\t\tswitch dlen {\n\t\tcase 8:\n\t\t\tdst[dsti+4] = dbuf[6]\u003c\u003c5 | dbuf[7]\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 7:\n\t\t\tdst[dsti+3] = dbuf[4]\u003c\u003c7 | dbuf[5]\u003c\u003c2 | dbuf[6]\u003e\u003e3\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 5:\n\t\t\tdst[dsti+2] = dbuf[3]\u003c\u003c4 | dbuf[4]\u003e\u003e1\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 4:\n\t\t\tdst[dsti+1] = dbuf[1]\u003c\u003c6 | dbuf[2]\u003c\u003c1 | dbuf[3]\u003e\u003e4\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 2:\n\t\t\tdst[dsti+0] = dbuf[0]\u003c\u003c3 | dbuf[1]\u003e\u003e2\n\t\t\tn++\n\t\t}\n\t\tdsti += 5\n\t}\n\treturn n, nil\n}\n\ntype encoder struct {\n\terr  error\n\tw    io.Writer\n\tenc  func(dst, src []byte)\n\tbuf  [5]byte    // buffered data waiting to be encoded\n\tnbuf int        // number of bytes in buf\n\tout  [1024]byte // output buffer\n}\n\nfunc NewEncoder(w io.Writer) io.WriteCloser {\n\treturn \u0026encoder{w: w, enc: Encode}\n}\n\nfunc NewEncoderLower(w io.Writer) io.WriteCloser {\n\treturn \u0026encoder{w: w, enc: EncodeLower}\n}\n\nfunc (e *encoder) Write(p []byte) (n int, err error) {\n\tif e.err != nil {\n\t\treturn 0, e.err\n\t}\n\n\t// Leading fringe.\n\tif e.nbuf \u003e 0 {\n\t\tvar i int\n\t\tfor i = 0; i \u003c len(p) \u0026\u0026 e.nbuf \u003c 5; i++ {\n\t\t\te.buf[e.nbuf] = p[i]\n\t\t\te.nbuf++\n\t\t}\n\t\tn += i\n\t\tp = p[i:]\n\t\tif e.nbuf \u003c 5 {\n\t\t\treturn\n\t\t}\n\t\te.enc(e.out[0:], e.buf[0:])\n\t\tif _, e.err = e.w.Write(e.out[0:8]); e.err != nil {\n\t\t\treturn n, e.err\n\t\t}\n\t\te.nbuf = 0\n\t}\n\n\t// Large interior chunks.\n\tfor len(p) \u003e= 5 {\n\t\tnn := len(e.out) / 8 * 5\n\t\tif nn \u003e len(p) {\n\t\t\tnn = len(p)\n\t\t\tnn -= nn % 5\n\t\t}\n\t\te.enc(e.out[0:], p[0:nn])\n\t\tif _, e.err = e.w.Write(e.out[0 : nn/5*8]); e.err != nil {\n\t\t\treturn n, e.err\n\t\t}\n\t\tn += nn\n\t\tp = p[nn:]\n\t}\n\n\t// Trailing fringe.\n\tcopy(e.buf[:], p)\n\te.nbuf = len(p)\n\tn += len(p)\n\treturn\n}\n\n// Close flushes any pending output from the encoder.\n// It is an error to call Write after calling Close.\nfunc (e *encoder) Close() error {\n\t// If there's anything left in the buffer, flush it out\n\tif e.err == nil \u0026\u0026 e.nbuf \u003e 0 {\n\t\te.enc(e.out[0:], e.buf[0:e.nbuf])\n\t\tencodedLen := EncodedLen(e.nbuf)\n\t\te.nbuf = 0\n\t\t_, e.err = e.w.Write(e.out[0:encodedLen])\n\t}\n\treturn e.err\n}\n\n// Decode decodes src using cford32. It writes at most\n// [DecodedLen](len(src)) bytes to dst and returns the number of bytes\n// written. If src contains invalid cford32 data, it will return the\n// number of bytes successfully written and [CorruptInputError].\n// Newline characters (\\r and \\n) are ignored.\nfunc Decode(dst, src []byte) (n int, err error) {\n\tbuf := make([]byte, len(src))\n\tl := stripNewlines(buf, src)\n\treturn decode(dst, buf[:l])\n}\n\n// AppendDecode appends the cford32 decoded src to dst\n// and returns the extended buffer.\n// If the input is malformed, it returns the partially decoded src and an error.\nfunc AppendDecode(dst, src []byte) ([]byte, error) {\n\tn := DecodedLen(len(src))\n\n\tdst = grow(dst, n)\n\tdstsl := dst[len(dst) : len(dst)+n]\n\tn, err := Decode(dstsl, src)\n\treturn dst[:len(dst)+n], err\n}\n\n// DecodeString returns the bytes represented by the cford32 string s.\nfunc DecodeString(s string) ([]byte, error) {\n\tbuf := []byte(s)\n\tl := stripNewlines(buf, buf)\n\tn, err := decode(buf, buf[:l])\n\treturn buf[:n], err\n}\n\n// stripNewlines removes newline characters and returns the number\n// of non-newline characters copied to dst.\nfunc stripNewlines(dst, src []byte) int {\n\toffset := 0\n\tfor _, b := range src {\n\t\tif b == '\\r' || b == '\\n' {\n\t\t\tcontinue\n\t\t}\n\t\tdst[offset] = b\n\t\toffset++\n\t}\n\treturn offset\n}\n\ntype decoder struct {\n\terr    error\n\tr      io.Reader\n\tbuf    [1024]byte // leftover input\n\tnbuf   int\n\tout    []byte // leftover decoded output\n\toutbuf [1024 / 8 * 5]byte\n}\n\n// NewDecoder constructs a new base32 stream decoder.\nfunc NewDecoder(r io.Reader) io.Reader {\n\treturn \u0026decoder{r: \u0026newlineFilteringReader{r}}\n}\n\nfunc readEncodedData(r io.Reader, buf []byte) (n int, err error) {\n\tfor n \u003c 1 \u0026\u0026 err == nil {\n\t\tvar nn int\n\t\tnn, err = r.Read(buf[n:])\n\t\tn += nn\n\t}\n\treturn\n}\n\nfunc (d *decoder) Read(p []byte) (n int, err error) {\n\t// Use leftover decoded output from last read.\n\tif len(d.out) \u003e 0 {\n\t\tn = copy(p, d.out)\n\t\td.out = d.out[n:]\n\t\tif len(d.out) == 0 {\n\t\t\treturn n, d.err\n\t\t}\n\t\treturn n, nil\n\t}\n\n\tif d.err != nil {\n\t\treturn 0, d.err\n\t}\n\n\t// Read nn bytes from input, bounded [8,len(d.buf)]\n\tnn := (len(p)/5 + 1) * 8\n\tif nn \u003e len(d.buf) {\n\t\tnn = len(d.buf)\n\t}\n\n\tnn, d.err = readEncodedData(d.r, d.buf[d.nbuf:nn])\n\td.nbuf += nn\n\tif d.nbuf \u003c 1 {\n\t\treturn 0, d.err\n\t}\n\n\t// Decode chunk into p, or d.out and then p if p is too small.\n\tnr := d.nbuf\n\tif d.err != io.EOF \u0026\u0026 nr%8 != 0 {\n\t\tnr -= nr % 8\n\t}\n\tnw := DecodedLen(d.nbuf)\n\n\tif nw \u003e len(p) {\n\t\tnw, err = decode(d.outbuf[0:], d.buf[0:nr])\n\t\td.out = d.outbuf[0:nw]\n\t\tn = copy(p, d.out)\n\t\td.out = d.out[n:]\n\t} else {\n\t\tn, err = decode(p, d.buf[0:nr])\n\t}\n\td.nbuf -= nr\n\tfor i := 0; i \u003c d.nbuf; i++ {\n\t\td.buf[i] = d.buf[i+nr]\n\t}\n\n\tif err != nil \u0026\u0026 (d.err == nil || d.err == io.EOF) {\n\t\td.err = err\n\t}\n\n\tif len(d.out) \u003e 0 {\n\t\t// We cannot return all the decoded bytes to the caller in this\n\t\t// invocation of Read, so we return a nil error to ensure that Read\n\t\t// will be called again.  The error stored in d.err, if any, will be\n\t\t// returned with the last set of decoded bytes.\n\t\treturn n, nil\n\t}\n\n\treturn n, d.err\n}\n\ntype newlineFilteringReader struct {\n\twrapped io.Reader\n}\n\nfunc (r *newlineFilteringReader) Read(p []byte) (int, error) {\n\tn, err := r.wrapped.Read(p)\n\tfor n \u003e 0 {\n\t\ts := p[0:n]\n\t\toffset := stripNewlines(s, s)\n\t\tif err != nil || offset \u003e 0 {\n\t\t\treturn offset, err\n\t\t}\n\t\t// Previous buffer entirely whitespace, read again\n\t\tn, err = r.wrapped.Read(p)\n\t}\n\treturn n, err\n}\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package cford32 implements a modified base32 encoding based on Douglas\n// Crockford's base32 encoding.\npackage cford32\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/cford32/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":"g31qVR9p3hILtbPD0mUsiY43OVwuLhueQnVfTChZAwg9I+hEHc1xJ7yz8GoStWfgsIgmwVtVW8ZyVCLz4bqcDg=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","package":{"name":"ufmt","path":"gno.land/p/nt/ufmt/v0","files":[{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package ufmt provides utility functions for formatting strings, similarly to\n// the Go package \"fmt\", of which only a subset is currently supported.\npackage ufmt\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/ufmt/v0\"\ngno = \"0.9\"\n"},{"name":"ufmt.gno","body":"// Package ufmt provides utility functions for formatting strings, similarly to\n// the Go package \"fmt\", of which only a subset is currently supported (hence\n// the name µfmt - micro fmt). It includes functions like Printf, Sprintf,\n// Fprintf, and Errorf.\n// Supported formatting verbs are documented in the Sprintf function.\npackage ufmt\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode/utf8\"\n)\n\n// buffer accumulates formatted output as a byte slice.\ntype buffer []byte\n\nfunc (b *buffer) write(p []byte) {\n\t*b = append(*b, p...)\n}\n\nfunc (b *buffer) writeString(s string) {\n\t*b = append(*b, s...)\n}\n\nfunc (b *buffer) writeByte(c byte) {\n\t*b = append(*b, c)\n}\n\nfunc (b *buffer) writeRune(r rune) {\n\t*b = utf8.AppendRune(*b, r)\n}\n\n// printer holds state for formatting operations.\ntype printer struct {\n\tbuf buffer\n}\n\nfunc newPrinter() *printer {\n\treturn \u0026printer{}\n}\n\n// Sprint formats using the default formats for its operands and returns the resulting string.\n// Sprint writes the given arguments with spaces between arguments.\nfunc Sprint(a ...any) string {\n\tp := newPrinter()\n\tp.doPrint(a)\n\treturn string(p.buf)\n}\n\n// doPrint formats arguments using default formats and writes to printer's buffer.\n// Spaces are added between arguments.\nfunc (p *printer) doPrint(args []any) {\n\tfor argNum, arg := range args {\n\t\tif argNum \u003e 0 {\n\t\t\tp.buf.writeRune(' ')\n\t\t}\n\n\t\tswitch v := arg.(type) {\n\t\tcase string:\n\t\t\tp.buf.writeString(v)\n\t\tcase (interface{ String() string }):\n\t\t\tp.buf.writeString(v.String())\n\t\tcase error:\n\t\t\tp.buf.writeString(v.Error())\n\t\tcase float64:\n\t\t\tp.buf.writeString(Sprintf(\"%f\", v))\n\t\tcase int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n\t\t\tp.buf.writeString(Sprintf(\"%d\", v))\n\t\tcase bool:\n\t\t\tif v {\n\t\t\t\tp.buf.writeString(\"true\")\n\t\t\t} else {\n\t\t\t\tp.buf.writeString(\"false\")\n\t\t\t}\n\t\tcase nil:\n\t\t\tp.buf.writeString(\"\u003cnil\u003e\")\n\t\tdefault:\n\t\t\tp.buf.writeString(\"(unhandled)\")\n\t\t}\n\t}\n}\n\n// doPrintln appends a newline after formatting arguments with doPrint.\nfunc (p *printer) doPrintln(a []any) {\n\tp.doPrint(a)\n\tp.buf.writeByte('\\n')\n}\n\n// Sprintf offers similar functionality to Go's fmt.Sprintf, or the sprintf\n// equivalent available in many languages, including C/C++.\n// The number of args passed must exactly match the arguments consumed by the format.\n// A limited number of formatting verbs and features are currently supported.\n//\n// Supported verbs:\n//\n//\t%s: Places a string value directly.\n//\t    If the value implements the interface interface{ String() string },\n//\t    the String() method is called to retrieve the value. Same about Error()\n//\t    string.\n//\t%c: Formats the character represented by Unicode code point\n//\t%d: Formats an integer value using package \"strconv\".\n//\t    Currently supports only uint, uint64, int, int64.\n//\t%f: Formats a float value, with a default precision of 6.\n//\t%e: Formats a float with scientific notation; 1.23456e+78\n//\t%E: Formats a float with scientific notation; 1.23456E+78\n//\t%F: The same as %f\n//\t%g: Formats a float value with %e for large exponents, and %f with full precision for smaller numbers\n//\t%G: Formats a float value with %G for large exponents, and %F with full precision for smaller numbers\n//\t%t: Formats a boolean value to \"true\" or \"false\".\n//\t%x: Formats an integer value as a hexadecimal string.\n//\t    Currently supports only uint8, []uint8, [32]uint8.\n//\t%c: Formats a rune value as a string.\n//\t    Currently supports only rune, int.\n//\t%q: Formats a string value as a quoted string.\n//\t%T: Formats the type of the value.\n//\t%v: Formats the value with a default representation appropriate for the value's type\n//\t    - nil: \u003cnil\u003e\n//\t    - bool: true/false\n//\t    - integers: base 10\n//\t    - float64: %g format\n//\t    - string: verbatim\n//\t    - types with String()/Error(): method result\n//\t    - others: (unhandled)\n//\t%%: Outputs a literal %. Does not consume an argument.\n//\n// Unsupported verbs or type mismatches produce error strings like \"%!d(string=foo)\".\nfunc Sprintf(format string, a ...any) string {\n\tp := newPrinter()\n\tp.doPrintf(format, a)\n\treturn string(p.buf)\n}\n\n// doPrintf parses the format string and writes formatted arguments to the buffer.\nfunc (p *printer) doPrintf(format string, args []any) {\n\tsTor := []rune(format)\n\tend := len(sTor)\n\targNum := 0\n\targLen := len(args)\n\n\tfor i := 0; i \u003c end; {\n\t\tisLast := i == end-1\n\t\tc := sTor[i]\n\n\t\tif isLast || c != '%' {\n\t\t\t// we don't check for invalid format like a one ending with \"%\"\n\t\t\tp.buf.writeRune(c)\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\n\t\tlength := -1\n\t\tprecision := -1\n\t\ti++ // skip '%'\n\n\t\tdigits := func() string {\n\t\t\tstart := i\n\t\t\tfor i \u003c end \u0026\u0026 sTor[i] \u003e= '0' \u0026\u0026 sTor[i] \u003c= '9' {\n\t\t\t\ti++\n\t\t\t}\n\t\t\tif i \u003e start {\n\t\t\t\treturn string(sTor[start:i])\n\t\t\t}\n\t\t\treturn \"\"\n\t\t}\n\n\t\tif l := digits(); l != \"\" {\n\t\t\tvar err error\n\t\t\tlength, err = strconv.Atoi(l)\n\t\t\tif err != nil {\n\t\t\t\tpanic(\"ufmt: invalid length specification\")\n\t\t\t}\n\t\t}\n\n\t\tif i \u003c end \u0026\u0026 sTor[i] == '.' {\n\t\t\ti++ // skip '.'\n\t\t\tif l := digits(); l != \"\" {\n\t\t\t\tvar err error\n\t\t\t\tprecision, err = strconv.Atoi(l)\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(\"ufmt: invalid precision specification\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif i \u003e= end {\n\t\t\tpanic(\"ufmt: invalid format string\")\n\t\t}\n\n\t\tverb := sTor[i]\n\t\tif verb == '%' {\n\t\t\tp.buf.writeRune('%')\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\n\t\tif argNum \u003e= argLen {\n\t\t\tpanic(\"ufmt: not enough arguments\")\n\t\t}\n\t\targ := args[argNum]\n\t\targNum++\n\n\t\tswitch verb {\n\t\tcase 'v':\n\t\t\twriteValue(p, verb, arg)\n\t\tcase 's':\n\t\t\twriteStringWithLength(p, verb, arg, length)\n\t\tcase 'c':\n\t\t\twriteChar(p, verb, arg)\n\t\tcase 'd':\n\t\t\twriteInt(p, verb, arg)\n\t\tcase 'e', 'E', 'f', 'F', 'g', 'G':\n\t\t\twriteFloatWithPrecision(p, verb, arg, precision)\n\t\tcase 't':\n\t\t\twriteBool(p, verb, arg)\n\t\tcase 'x':\n\t\t\twriteHex(p, verb, arg)\n\t\tcase 'q':\n\t\t\twriteQuotedString(p, verb, arg)\n\t\tcase 'T':\n\t\t\twriteType(p, arg)\n\t\t// % handled before, as it does not consume an argument\n\t\tdefault:\n\t\t\tp.buf.writeString(\"(unhandled verb: %\" + string(verb) + \")\")\n\t\t}\n\n\t\ti++\n\t}\n\n\tif argNum \u003c argLen {\n\t\tpanic(\"ufmt: too many arguments\")\n\t}\n}\n\n// writeValue handles %v formatting\nfunc writeValue(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\tcase nil:\n\t\tp.buf.writeString(\"\u003cnil\u003e\")\n\tcase bool:\n\t\twriteBool(p, verb, v)\n\tcase int:\n\t\tp.buf.writeString(strconv.Itoa(v))\n\tcase int8:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase int16:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase int32:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase int64:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase uint:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint8:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint16:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint32:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint64:\n\t\tp.buf.writeString(strconv.FormatUint(v, 10))\n\tcase float64:\n\t\tp.buf.writeString(strconv.FormatFloat(v, 'g', -1, 64))\n\tcase string:\n\t\tp.buf.writeString(v)\n\tcase []byte:\n\t\tp.buf.write(v)\n\tcase []rune:\n\t\tp.buf.writeString(string(v))\n\tcase (interface{ String() string }):\n\t\tp.buf.writeString(v.String())\n\tcase error:\n\t\tp.buf.writeString(v.Error())\n\tdefault:\n\t\tp.buf.writeString(fallback(verb, v))\n\t}\n}\n\n// writeStringWithLength handles %s formatting with length specification\nfunc writeStringWithLength(p *printer, verb rune, arg any, length int) {\n\tvar s string\n\tswitch v := arg.(type) {\n\tcase (interface{ String() string }):\n\t\ts = v.String()\n\tcase error:\n\t\ts = v.Error()\n\tcase string:\n\t\ts = v\n\tdefault:\n\t\ts = fallback(verb, v)\n\t}\n\n\tif length \u003e 0 \u0026\u0026 len(s) \u003c length {\n\t\ts = strings.Repeat(\" \", length-len(s)) + s\n\t}\n\tp.buf.writeString(s)\n}\n\n// writeChar handles %c formatting\nfunc writeChar(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\t// rune is int32. Exclude overflowing numeric types and dups (byte, int32):\n\tcase rune:\n\t\tp.buf.writeString(string(v))\n\tcase int:\n\t\tp.buf.writeRune(rune(v))\n\tcase int8:\n\t\tp.buf.writeRune(rune(v))\n\tcase int16:\n\t\tp.buf.writeRune(rune(v))\n\tcase uint:\n\t\tp.buf.writeRune(rune(v))\n\tcase uint8:\n\t\tp.buf.writeRune(rune(v))\n\tcase uint16:\n\t\tp.buf.writeRune(rune(v))\n\tdefault:\n\t\tp.buf.writeString(fallback(verb, v))\n\t}\n}\n\n// writeInt handles %d formatting\nfunc writeInt(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\tcase int:\n\t\tp.buf.writeString(strconv.Itoa(v))\n\tcase int8:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase int16:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase int32:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase int64:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase uint:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint8:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint16:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint32:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint64:\n\t\tp.buf.writeString(strconv.FormatUint(v, 10))\n\tdefault:\n\t\tp.buf.writeString(fallback(verb, v))\n\t}\n}\n\n// writeFloatWithPrecision handles floating-point formatting with precision\nfunc writeFloatWithPrecision(p *printer, verb rune, arg any, precision int) {\n\tswitch v := arg.(type) {\n\tcase float64:\n\t\tformat := byte(verb)\n\t\tif format == 'F' {\n\t\t\tformat = 'f'\n\t\t}\n\t\tif precision \u003c 0 {\n\t\t\tswitch format {\n\t\t\tcase 'e', 'E':\n\t\t\t\tprecision = 2\n\t\t\tdefault:\n\t\t\t\tprecision = 6\n\t\t\t}\n\t\t}\n\t\tp.buf = strconv.AppendFloat(p.buf, v, format, precision, 64)\n\tdefault:\n\t\tp.buf.writeString(fallback(verb, v))\n\t}\n}\n\n// writeBool handles %t formatting\nfunc writeBool(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\tcase bool:\n\t\tif v {\n\t\t\tp.buf.writeString(\"true\")\n\t\t} else {\n\t\t\tp.buf.writeString(\"false\")\n\t\t}\n\tdefault:\n\t\tp.buf.writeString(fallback(verb, v))\n\t}\n}\n\n// writeHex handles %x formatting\nfunc writeHex(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\tcase uint8:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 16))\n\tdefault:\n\t\tp.buf.writeString(\"(unhandled)\")\n\t}\n}\n\n// writeQuotedString handles %q formatting\nfunc writeQuotedString(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\tcase string:\n\t\tp.buf.writeString(strconv.Quote(v))\n\tdefault:\n\t\tp.buf.writeString(\"(unhandled)\")\n\t}\n}\n\n// writeType handles %T formatting\nfunc writeType(p *printer, arg any) {\n\tswitch arg.(type) {\n\tcase bool:\n\t\tp.buf.writeString(\"bool\")\n\tcase int:\n\t\tp.buf.writeString(\"int\")\n\tcase int8:\n\t\tp.buf.writeString(\"int8\")\n\tcase int16:\n\t\tp.buf.writeString(\"int16\")\n\tcase int32:\n\t\tp.buf.writeString(\"int32\")\n\tcase int64:\n\t\tp.buf.writeString(\"int64\")\n\tcase uint:\n\t\tp.buf.writeString(\"uint\")\n\tcase uint8:\n\t\tp.buf.writeString(\"uint8\")\n\tcase uint16:\n\t\tp.buf.writeString(\"uint16\")\n\tcase uint32:\n\t\tp.buf.writeString(\"uint32\")\n\tcase uint64:\n\t\tp.buf.writeString(\"uint64\")\n\tcase string:\n\t\tp.buf.writeString(\"string\")\n\tcase []byte:\n\t\tp.buf.writeString(\"[]byte\")\n\tcase []rune:\n\t\tp.buf.writeString(\"[]rune\")\n\tdefault:\n\t\tp.buf.writeString(\"unknown\")\n\t}\n}\n\n// Fprintf formats according to a format specifier and writes to w.\n// Returns the number of bytes written and any write error encountered.\nfunc Fprintf(w io.Writer, format string, a ...any) (n int, err error) {\n\tp := newPrinter()\n\tp.doPrintf(format, a)\n\treturn w.Write(p.buf)\n}\n\n// Printf formats according to a format specifier and writes to standard output.\n// Returns the number of bytes written and any write error encountered.\n//\n// XXX: Replace with os.Stdout handling when available.\nfunc Printf(format string, a ...any) (n int, err error) {\n\tvar out strings.Builder\n\tn, err = Fprintf(\u0026out, format, a...)\n\tprint(out.String())\n\treturn n, err\n}\n\n// Appendf formats according to a format specifier, appends the result to the byte\n// slice, and returns the updated slice.\nfunc Appendf(b []byte, format string, a ...any) []byte {\n\tp := newPrinter()\n\tp.doPrintf(format, a)\n\treturn append(b, p.buf...)\n}\n\n// Fprint formats using default formats and writes to w.\n// Spaces are added between arguments.\n// Returns the number of bytes written and any write error encountered.\nfunc Fprint(w io.Writer, a ...any) (n int, err error) {\n\tp := newPrinter()\n\tp.doPrint(a)\n\treturn w.Write(p.buf)\n}\n\n// Print formats using default formats and writes to standard output.\n// Spaces are added between arguments.\n// Returns the number of bytes written and any write error encountered.\n//\n// XXX: Replace with os.Stdout handling when available.\nfunc Print(a ...any) (n int, err error) {\n\tvar out strings.Builder\n\tn, err = Fprint(\u0026out, a...)\n\tprint(out.String())\n\treturn n, err\n}\n\n// Append formats using default formats, appends to b, and returns the updated slice.\n// Spaces are added between arguments.\nfunc Append(b []byte, a ...any) []byte {\n\tp := newPrinter()\n\tp.doPrint(a)\n\treturn append(b, p.buf...)\n}\n\n// Fprintln formats using default formats and writes to w with newline.\n// Returns the number of bytes written and any write error encountered.\nfunc Fprintln(w io.Writer, a ...any) (n int, err error) {\n\tp := newPrinter()\n\tp.doPrintln(a)\n\treturn w.Write(p.buf)\n}\n\n// Println formats using default formats and writes to standard output with newline.\n// Returns the number of bytes written and any write error encountered.\n//\n// XXX: Replace with os.Stdout handling when available.\nfunc Println(a ...any) (n int, err error) {\n\tvar out strings.Builder\n\tn, err = Fprintln(\u0026out, a...)\n\tprint(out.String())\n\treturn n, err\n}\n\n// Sprintln formats using default formats and returns the string with newline.\n// Spaces are always added between arguments.\nfunc Sprintln(a ...any) string {\n\tp := newPrinter()\n\tp.doPrintln(a)\n\treturn string(p.buf)\n}\n\n// Appendln formats using default formats, appends to b, and returns the updated slice.\n// Appends a newline after the last argument.\nfunc Appendln(b []byte, a ...any) []byte {\n\tp := newPrinter()\n\tp.doPrintln(a)\n\treturn append(b, p.buf...)\n}\n\n// This function is used to mimic Go's fmt.Sprintf\n// specific behaviour of showing verb/type mismatches,\n// where for example:\n//\n//\tfmt.Sprintf(\"%d\", \"foo\") gives \"%!d(string=foo)\"\n//\n// Here:\n//\n//\tfallback(\"s\", 8) -\u003e \"%!s(int=8)\"\n//\tfallback(\"d\", nil) -\u003e \"%!d(\u003cnil\u003e)\", and so on.f\nfunc fallback(verb rune, arg any) string {\n\tvar s string\n\tswitch v := arg.(type) {\n\tcase string:\n\t\ts = \"string=\" + v\n\tcase (interface{ String() string }):\n\t\ts = \"string=\" + v.String()\n\tcase error:\n\t\t// note: also \"string=\" in Go fmt\n\t\ts = \"string=\" + v.Error()\n\tcase float64:\n\t\ts = \"float64=\" + Sprintf(\"%f\", v)\n\tcase int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n\t\t// note: rune, byte would be dups, being aliases\n\t\tif typename, e := typeToString(v); e == nil {\n\t\t\ts = typename + \"=\" + Sprintf(\"%d\", v)\n\t\t} else {\n\t\t\tpanic(\"ufmt: unexpected type error\")\n\t\t}\n\tcase bool:\n\t\ts = \"bool=\" + strconv.FormatBool(v)\n\tcase nil:\n\t\ts = \"\u003cnil\u003e\"\n\tdefault:\n\t\ts = \"(unhandled)\"\n\t}\n\treturn \"%!\" + string(verb) + \"(\" + s + \")\"\n}\n\n// typeToString returns the name of basic Go types as string.\nfunc typeToString(v any) (string, error) {\n\tswitch v.(type) {\n\tcase string:\n\t\treturn \"string\", nil\n\tcase int:\n\t\treturn \"int\", nil\n\tcase int8:\n\t\treturn \"int8\", nil\n\tcase int16:\n\t\treturn \"int16\", nil\n\tcase int32:\n\t\treturn \"int32\", nil\n\tcase int64:\n\t\treturn \"int64\", nil\n\tcase uint:\n\t\treturn \"uint\", nil\n\tcase uint8:\n\t\treturn \"uint8\", nil\n\tcase uint16:\n\t\treturn \"uint16\", nil\n\tcase uint32:\n\t\treturn \"uint32\", nil\n\tcase uint64:\n\t\treturn \"uint64\", nil\n\tcase float32:\n\t\treturn \"float32\", nil\n\tcase float64:\n\t\treturn \"float64\", nil\n\tcase bool:\n\t\treturn \"bool\", nil\n\tdefault:\n\t\treturn \"\", errors.New(\"unsupported type\")\n\t}\n}\n\n// errMsg implements the error interface for formatted error strings.\ntype errMsg struct {\n\tmsg string\n}\n\n// Error returns the formatted error message.\nfunc (e *errMsg) Error() string {\n\treturn e.msg\n}\n\n// Errorf formats according to a format specifier and returns an error value.\n// Supports the same verbs as Sprintf. See Sprintf documentation for details.\nfunc Errorf(format string, args ...any) error {\n\treturn \u0026errMsg{Sprintf(format, args...)}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":"mcS9+Ie6ztHEk+NiPcXWHcvq4+PT8+lS7OP+YvtThHsj3pRsm3zsoOARLaf69WZZ6xlWkpzmqXyOHbjS6Euj8A=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","package":{"name":"governor","path":"gno.land/p/kourt/governor/v0","files":[{"name":"batch.gno","body":"package governor\n\nimport (\n\t\"strings\"\n\n\tufmt \"gno.land/p/nt/ufmt/v0\"\n)\n\n// batchKind runs several adopted kinds as one decision.\n//\n//\tgovern:batch   \"treasury/spend 100\\nfee 250\"\n//\n// One member per line, the kind first and the rest its payload. Members must\n// already be adopted: a batch decides several things at once and smuggles in\n// nothing.\n//\n// All or nothing, because there is no halfway. A member's Do returning an error\n// makes this panic, aborting the transaction and unwinding what earlier members\n// did — the only atomicity gno offers.\n//\n// So a failed batch leaves the proposal Succeeded rather than Failed: the\n// transaction wrote nothing, including the failure. It can be run again and\n// will fail identically until the world changes or the grace period ends it.\n//\n// Single-member kinds keep the other behaviour — record the failure and finish\n// — since there is nothing to be atomic with respect to.\ntype batchKind struct{}\n\n// maxBatch bounds a batch, because Describe and Check both walk it and a\n// proposal nobody can render is a proposal nobody can vote on.\nconst maxBatch = 16\n\nfunc (k batchKind) Name() string { return reserved + \"batch\" }\n\nfunc (k batchKind) describe(g *Governor, payload string) string {\n\tmembers, err := parseBatch(payload)\n\tif err != nil {\n\t\treturn \"malformed batch: \" + err.Error()\n\t}\n\tout := ufmt.Sprintf(\"do all %d of these, or none of them:\\n\", len(members))\n\tfor i, m := range members {\n\t\t// Each member described by its own kind, from its own payload — the\n\t\t// same strings their Do will be handed. A batch that summarised its\n\t\t// members in its own words would be the description gap this design\n\t\t// exists to avoid, reintroduced one level up.\n\t\tout += ufmt.Sprintf(\"\\n%d. **%s** — %s\", i+1, m.kind, g.describeMember(m))\n\t}\n\treturn out\n}\n\nfunc (g *Governor) describeMember(m member) string {\n\tk := g.anyKind(m.kind)\n\tif k == nil {\n\t\treturn \"_no kind by that name_\"\n\t}\n\treturn g.describeKind(k, m.payload)\n}\n\nfunc (k batchKind) check(g *Governor, payload string) error {\n\tmembers, err := parseBatch(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, m := range members {\n\t\te := g.entryOf(m.kind)\n\t\tif e == nil {\n\t\t\treturn govErr(\"no kind called \" + m.kind)\n\t\t}\n\t\tif !e.live {\n\t\t\t// A batch cannot reach a power the holders have not granted.\n\t\t\treturn govErr(m.kind + \" is not adopted\")\n\t\t}\n\t\tif m.kind == (batchKind{}).Name() {\n\t\t\t// No batches of batches. Nesting makes the rendered description a\n\t\t\t// tree of unbounded depth, and the point of the description is\n\t\t\t// that somebody reads it.\n\t\t\treturn govErr(\"a batch cannot contain a batch\")\n\t\t}\n\t\tif err := g.checkKind(e.kind, m.payload); err != nil {\n\t\t\treturn govErr(m.kind + \": \" + err.Error())\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (k batchKind) run(g *Governor, dispatch Dispatch, payload string) error {\n\tmembers, err := parseBatch(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor i, m := range members {\n\t\te := g.entryOf(m.kind)\n\t\tif e == nil || !e.live {\n\t\t\tpanic(\"govern: batch member \" + m.kind + \" is no longer adopted\")\n\t\t}\n\t\tvar err error\n\t\tif b, ok := asBuiltin(e.kind); ok {\n\t\t\terr = b.run(g, dispatch, m.payload)\n\t\t} else {\n\t\t\t// Each member gets its OWN sub-realm, named for the power it is,\n\t\t\t// rather than the batch's. A batch is a way to decide several\n\t\t\t// things at once, not a way to launder one kind's authority into\n\t\t\t// another's.\n\t\t\terr = dispatch(e.kind, subPathOf(m.kind), m.payload)\n\t\t}\n\t\tif err != nil {\n\t\t\t// Panic, not return. Returning would record a failure and keep\n\t\t\t// whatever the earlier members already did, which is the one\n\t\t\t// outcome a batch promises cannot happen.\n\t\t\tpanic(ufmt.Sprintf(\"govern: batch member %d (%s) failed: %s\",\n\t\t\t\ti+1, m.kind, err.Error()))\n\t\t}\n\t}\n\treturn nil\n}\n\ntype member struct {\n\tkind    string\n\tpayload string\n}\n\nfunc parseBatch(payload string) ([]member, error) {\n\tvar out []member\n\tfor _, line := range strings.Split(payload, \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tsp := strings.Index(line, \" \")\n\t\tif sp \u003c= 0 {\n\t\t\t// A member with no payload is almost always a typo, and a batch is\n\t\t\t// the wrong place to find out.\n\t\t\treturn nil, govErr(\"expected `\u003ckind\u003e \u003cpayload\u003e`, got: \" + line)\n\t\t}\n\t\tout = append(out, member{kind: line[:sp], payload: strings.TrimSpace(line[sp+1:])})\n\t}\n\tif len(out) \u003c 2 {\n\t\treturn nil, govErr(\"a batch needs at least two members; propose the kind directly\")\n\t}\n\tif len(out) \u003e maxBatch {\n\t\treturn nil, govErr(\"too many members in one batch\")\n\t}\n\treturn out, nil\n}\n"},{"name":"builtin.gno","body":"package governor\n\n// The governor's own kinds act on the governor, and take it as an argument.\n//\n// They cannot hold one. A kind is a value in that governor's own registry, so a\n// back-pointer is a reference cycle — and the VM refuses to finalize it:\n//\n//\tunexpected unreal object: type=*gnolang.HeapItemValue ... isNewReal=true\n//\n// Every test passed and the realm's own filetests wrote their storage; the\n// panic came afterwards, at persistence, which is the one place a cycle shows\n// up and the reason it survived being reasoned about. So the built-ins are\n// stateless values and the engine hands them the governor at the moment it\n// dispatches.\n//\n// The three methods mirror Kind's, minus the realm token: a built-in acts on\n// the registry rather than on the world, so it has no use for a sub-realm and\n// is never given one.\ntype builtin interface {\n\tName() string\n\tdescribe(g *Governor, payload string) string\n\tcheck(g *Governor, payload string) error\n\trun(g *Governor, dispatch Dispatch, payload string) error\n}\n\n// asBuiltin reports whether a kind is one of the governor's own.\n//\n// A type assertion rather than a name check. The reserved prefix says what a\n// kind is CALLED and Offer refuses it, but the dispatch has to be decided by\n// what a kind IS — a nominal check is the one thing an impostor cannot satisfy,\n// which is the same argument grc20's IsCanonicalTeller makes.\nfunc asBuiltin(k Kind) (builtin, bool) {\n\tb, ok := k.(builtin)\n\treturn b, ok\n}\n\n// The Kind side of a built-in, so it satisfies the interface the registry\n// stores. Reached only through the engine, which checks asBuiltin first.\nfunc (k adoptKind) Describe(payload string) string { return \"\" }\nfunc (k adoptKind) Check(payload string) error     { return errBuiltinNeedsEngine }\nfunc (k adoptKind) Do(_ int, rlm realm, payload string) error {\n\treturn errBuiltinNeedsEngine\n}\n\nfunc (k retireKind) Describe(payload string) string { return \"\" }\nfunc (k retireKind) Check(payload string) error     { return errBuiltinNeedsEngine }\nfunc (k retireKind) Do(_ int, rlm realm, payload string) error {\n\treturn errBuiltinNeedsEngine\n}\n\nfunc (k rulesKind) Describe(payload string) string { return \"\" }\nfunc (k rulesKind) Check(payload string) error     { return errBuiltinNeedsEngine }\nfunc (k rulesKind) Do(_ int, rlm realm, payload string) error {\n\treturn errBuiltinNeedsEngine\n}\n\nfunc (k batchKind) Describe(payload string) string { return \"\" }\nfunc (k batchKind) Check(payload string) error     { return errBuiltinNeedsEngine }\nfunc (k batchKind) Do(_ int, rlm realm, payload string) error {\n\treturn errBuiltinNeedsEngine\n}\n\n// errBuiltinNeedsEngine is what these return if anything ever reaches them the\n// ordinary way. Unreachable through the engine, and an error rather than a\n// panic because a kind returning one is a proposal that fails cleanly.\nvar errBuiltinNeedsEngine = govErr(\"a built-in kind has to be run by the governor that owns it\")\n\n// checkKind and describeKind route to whichever half a kind has.\nfunc (g *Governor) checkKind(k Kind, payload string) error {\n\tif b, ok := asBuiltin(k); ok {\n\t\treturn b.check(g, payload)\n\t}\n\treturn k.Check(payload)\n}\n\nfunc (g *Governor) describeKind(k Kind, payload string) string {\n\tif b, ok := asBuiltin(k); ok {\n\t\treturn b.describe(g, payload)\n\t}\n\treturn k.Describe(payload)\n}\n"},{"name":"electorate.gno","body":"package governor\n\nimport (\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\n// Electorate is where voting weight comes from, and the only thing tying this\n// engine to any particular token. Everything else here is about proposals, so\n// with this extracted the engine can be driven by anything that can answer\n// four questions.\n//\n// It also makes the governor testable without a ledger. Reaching exactly a\n// quorum with real balances means solving for the supply, and a test that does\n// arithmetic to test arithmetic proves nothing.\n//\n// EVERY QUESTION HERE IS ABOUT A SEALED PAST EPOCH, which is what keeps the\n// Token/Electorate split below honest — Electorate about then, Token about now.\n// A live per-address read (VotesOf) was added here while fixing rented weight and\n// removed again when the fix was reverted; RENTEDWEIGHT.md records why. Anything\n// re-adding it is re-opening that decision, not making a small change.\n//\n// AND THE FIX THAT SHIPPED DID NOT NEED IT, which is the point worth keeping. The\n// live figure arrives as a CAP from the consumer (VoteWithCap) instead of as an\n// interface method the engine calls, so this interface stays entirely about THEN.\n// The engine cannot read a live balance even if it wanted to, and a supplied\n// ceiling can only lower what the engine derived — so coherence is structural\n// rather than something each implementor has to get right.\ntype Electorate interface {\n\t// PastVotes is what an address could vote at a sealed epoch.\n\tPastVotes(who address, at uint32) int64\n\n\t// PastTotal is everything that existed then, which quorum is a fraction\n\t// of. Taken at the same epoch as the votes it is compared against, or a\n\t// quorum could be met against a supply that no longer exists.\n\t//\n\t// It must be at least the sum of every PastVotes at that epoch. The\n\t// governor subtracts what has been cast from this to decide whether a\n\t// question is already settled, so a roll whose parts exceed its whole\n\t// reports votes that cannot exist and settles early. Measured, not feared:\n\t// one voter reporting a hundred against a claimed total of ten carried a\n\t// proposal at a 99% threshold.\n\t//\n\t// This realm's token satisfies it exactly, and a test says so after every\n\t// operation that moves power. Anything swapped in here needs its own. The\n\t// governor cannot check it and cannot recover from it — a clamp was tried.\n\t//\n\t// It must also stay at or below maxWeighable (MaxInt64/bps): the tally\n\t// computes yes*bps, and a supply above that overflows int64 into a negative\n\t// tally — a won vote reported as lost. grc20votes caps its supply there\n\t// exactly; the governor refuses a proposal whose snapshot supply exceeds it,\n\t// so a swapped-in electorate that ignores the ceiling fails loudly at the door.\n\tPastTotal(at uint32) int64\n\n\t// EngagedTotal is the part of PastTotal that quorum is measured against.\n\t//\n\t// It exists so a realm can drop idle weight out of the denominator without\n\t// touching anything in the governor: a fixed low bar is unreachable at\n\t// launch and trivial later, and moving the denominator is the fix that\n\t// keeps the bar meaningful at both ends.\n\t//\n\t// Should be \u003c= PastTotal at the same epoch. Weight outside it can still vote\n\t// — it simply is not counted in the bar — so the governor keeps using\n\t// PastTotal for how many votes could still be cast. If it exceeds PastTotal\n\t// the governor clamps it down to PastTotal, so an electorate that over-reports\n\t// engagement makes the bar unreachable-but-valid rather than aborting.\n\t//\n\t// This realm's token returns PastTotal unchanged: every holder is engaged\n\t// by definition, since power is self-delegated by default.\n\tEngagedTotal(at uint32) int64\n\n\t// Epoch is now. A proposal snapshots Epoch()-1 and stores it; nothing\n\t// re-derives it later.\n\tEpoch() uint32\n\n\t// Height is the block height the governor gates on: when voting opens and\n\t// closes, when a delay has elapsed, when a proposal may execute.\n\t//\n\t// It comes from the electorate rather than from the chain so that a realm\n\t// whose clock can be fast-forwarded moves the VOTES with it. Reading\n\t// runtime.ChainHeight() here instead would leave a test chain able to age\n\t// everything except the one thing a dispute waits on, and would let the\n\t// governor and the ledger disagree about what epoch it is.\n\tHeight() int64\n\n\t// Now is the block's wall-clock time in unix seconds, from the same source\n\t// as Height and for the same reason: a realm that can fast-forward its clock\n\t// must move the VOTES with it, and a pure package may not read the chain\n\t// directly without leaving a test chain able to age everything except the\n\t// one thing a dispute waits on.\n\t//\n\t// It exists because a voting window is a PROMISE TO A VOTER — \"this closes\n\t// in four days\" — and a height only means a date if the chain's pace never\n\t// changes. The governor gates on this and keeps the height beside it.\n\tNow() int64\n}\n\n// Token is what a page needs to name the thing being voted: an identity and a\n// live supply. Separate from Electorate because they answer about different\n// times — Electorate about a sealed epoch, Token about now — and a realm may\n// well have one without the other.\n//\n// p/kourt/grc20votes.Ledger satisfies both, which is the ordinary case.\ntype Token interface {\n\tName() string\n\tSymbol() string\n\tDecimals() int\n\tTotalSupply() int64\n}\n\n// New creates a governor with no kinds adopted and nothing proposed, and\n// installs the built-in kinds that govern the governor itself.\n//\n// The minter kind is NOT among them: who may create supply is a policy about a\n// caller, and a pure package has no caller. A realm wanting one registers it\n// with InstallBuiltin.\nfunc New(voters Electorate, token Token) *Governor {\n\tif voters == nil || token == nil {\n\t\tpanic(\"governor: a governor needs an electorate and a token\")\n\t}\n\tg := \u0026Governor{\n\t\tvoters:    voters,\n\t\ttoken:     token,\n\t\tkinds:     bptree.NewBPTree32(),\n\t\tproposals: bptree.NewBPTree32(),\n\t\topenIdx:   bptree.NewBPTree32(),\n\t}\n\tg.installBuiltins()\n\treturn g\n}\n\n// InstallBuiltin adopts a kind under the reserved prefix, which Offer refuses.\n//\n// For the consuming realm only, and it is unexported state that makes that\n// true: reaching this needs the *Governor, which the realm keeps to itself.\n// A realm uses it for a power that is its own rather than the engine's — the\n// mint being the example.\nfunc (g *Governor) InstallBuiltin(k Kind, r Rules) {\n\tname := k.Name()\n\tif !isReserved(name) {\n\t\tpanic(\"governor: a built-in has to carry the reserved prefix\")\n\t}\n\tmustBeUsableName(name)\n\tg.mustBeSaneRules(r)\n\tg.kinds.Set(name, \u0026entry{kind: k, rules: r, live: true})\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/kourt/governor/v0\"\ngno = \"0.9\"\n"},{"name":"governor.gno","body":"package governor\n\nimport (\n\t\"chain\"\n\t\"crypto/sha256\"\n\t\"strconv\"\n\t\"strings\"\n\n\tbptree \"gno.land/p/nt/bptree/v0\"\n\tufmt \"gno.land/p/nt/ufmt/v0\"\n)\n\n// A proposal is a KIND and a PAYLOAD, both strings.\n//\n// Forced, and better than the alternative. MsgCall.Args is []string\n// (gno.land/pkg/sdk/vm/msgs.go) and convertArgToGno switches on\n// gno.BaseOf(argT), so scalars and named scalars like `address` convert; a\n// struct, interface, pointer or func panics with \"unexpected type in contract\n// arg\". An entrypoint taking one is uncallable by transaction.\n//\n// A []byte converts too, base64-decoded, so the string is a choice: a payload\n// arrives as text a voter can read, where bytes would arrive as base64.\n//\n// A proposer cannot supply a closure either. A realm can persist one, but\n// MsgRun forces the ephemeral package private and the save walk refuses it\n// (realm.go, \"cannot persist function or method from the private realm\").\n//\n// The property that falls out is what OpenZeppelin cannot offer: Describe and\n// Do consume the identical string, so what voters read IS what runs.\ntype Kind interface {\n\t// Name is the registry key.\n\tName() string\n\n\t// Describe renders a payload for the people voting on it.\n\t//\n\t// It must be a total function of the payload ALONE. Read live state here\n\t// and two voters at different heights are deciding different questions\n\t// while looking at the same page.\n\tDescribe(payload string) string\n\n\t// Check validates a payload against the world as it is now. Called when\n\t// the proposal opens AND again immediately before Do.\n\t//\n\t// The second call is the point. A proposal that was sound a week ago need\n\t// not be sound now, and the alternative is discovering that inside Do,\n\t// half-applied.\n\tCheck(payload string) error\n\n\t// Do performs it. Non-crossing, taking the realm as data, so a /p/ package\n\t// could implement this unchanged — crossing functions are illegal outside\n\t// a realm.\n\t//\n\t// Returning an error and panicking are different decisions. An error\n\t// FINISHES the proposal: recorded failed, slot returned, no retry. Say so\n\t// when the answer will not change.\n\t//\n\t// A panic ABORTS the transaction, writing nothing — including the failure\n\t// — so the proposal stays Succeeded and anybody may try again. Say so when\n\t// a retry could work, or when half-finishing is worse than not starting:\n\t// an abort is the only rollback gno offers. govern:batch depends on it.\n\tDo(_ int, rlm realm, payload string) error\n}\n\n// Rules are per kind, never global. Renaming a parameter and spending the\n// reserve should not need the same turnout, which is why one global quorum is\n// always wrong for something.\n//\n// Basis points as int64, never float64: gno's own govdao compares against a\n// float 66.66, and a threshold decided by binary floating point is a rounding\n// argument waiting to happen. yes*bps \u003e= (yes+no)*ThresholdBps is exact.\n//\n// bps is named once because it is load-bearing in three places at once: the\n// tally, the rendered percentages and the supply ceiling. A scale written out\n// separately in each is a number that can come to disagree with itself.\nconst bps = int64(10000)\n\n// maxWeighable is the largest snapshot supply a tally can weigh without yes*bps\n// overflowing int64 (and going negative, which reads a won vote as lost). The\n// Electorate contract requires PastTotal to stay within it; grc20votes caps its\n// own supply here exactly, so for that token this ceiling is never approached. It\n// is checked at the Propose door for a swapped-in electorate that does not.\nconst maxWeighable = int64(9223372036854775807) / bps\n\ntype Rules struct {\n\tQuorumBps int64 // of the snapshot supply, counting abstain as turnout\n\n\t// ThresholdBps is the share of yes+no (abstain excluded) that yes must reach.\n\t// The comparison is INCLUSIVE — yes*bps \u003e= (yes+no)*ThresholdBps — so at 5000\n\t// a 50-50 tie PASSES. A realm wanting a strict majority sets 5001 or more; the\n\t// bootstrap and gno's govdao use a 6600 supermajority.\n\tThresholdBps int64\n\n\t// ProposeBps is what a proposer must hold, as a fraction of the snapshot\n\t// supply, to open a question at all.\n\t//\n\t// Attention control, not spam control: opening a proposal already costs a\n\t// new key's worth of deposit. The list is what holders read, and a list\n\t// nobody reads is a governor nobody governs. Zero means anyone may\n\t// propose — right for a small realm, wrong for a public token.\n\tProposeBps   int64\n\tVotingBlocks int64\n\tDelayBlocks  int64 // succeeded -\u003e executable; the timelock\n\tGraceBlocks  int64 // executable -\u003e expired\n}\n\nconst (\n\tstateActive int8 = iota\n\tstateDefeated\n\tstateSucceeded\n\tstateExecuted\n\tstateFailed\n\tstateCanceled\n\tstateExpired\n)\n\n// proposal is one question, frozen.\ntype proposal struct {\n\tid       int64\n\tkind     string\n\tpayload  string\n\ttitle    string\n\tproposer address\n\n\t// Both copied when the proposal opens: the rules, so a later change cannot\n\t// move the bar earlier voters faced; the snapshot epoch, so the electorate\n\t// is the one that existed when the question was asked.\n\trules Rules\n\tepoch uint32\n\ttotal int64 // supply at that epoch, so quorum has a fixed denominator\n\n\t// engaged is the quorum's DENOMINATOR; total is everything in issue. Equal\n\t// for this realm's token and not necessarily for another: an electorate may\n\t// drop idle weight out of the bar while that weight can still vote. So\n\t// quorum divides by engaged, and \"what could still be cast\" divides by\n\t// total.\n\tengaged int64\n\n\t// quorumFloor, when positive, is an ABSOLUTE turnout the proposal needs\n\t// instead of the rules' QuorumBps fraction of engaged — a figure the consumer\n\t// computed itself (a court's max(5% supply, min(1×X̄, ⅓ votable))) and passed\n\t// through ProposeWithQuorum. Snapshotted here like every other bar, so it\n\t// cannot move under an open vote. Zero means the bps formula applies, which is\n\t// every ordinary caller.\n\tquorumFloor int64\n\n\topened int64\n\tcloses int64\n\t// The same two moments in unix seconds, written beside the heights and never\n\t// derived afterwards. A VOTING WINDOW IS A PROMISE TO A VOTER, and a height\n\t// only means a date if the chain's pace never changes; a proposal opened\n\t// before these existed carries 0 and keeps the height it was opened under.\n\topenedTime int64\n\tclosesTime int64\n\tready      int64 // set when it succeeds\n\treadyTime  int64 // its wall-clock twin, written at the same moment\n\tstate  int8\n\treason string\n\n\tyes, no, abstain int64\n\t// voted exists only to refuse a second vote from the same address. Nothing\n\t// renders it and nothing else reads it.\n\t//\n\t// A tree per proposal rather than one shared tree keyed by (id, voter),\n\t// for cleanup rather than cost: this buys a whole node on the first vote\n\t// (~4,500 bytes) where a shared tree would cost one entry, but dropping it\n\t// is one assignment and the deposit goes back to whoever settles. Clearing\n\t// a range out of a shared tree is a removal per voter, unbounded, and\n\t// bptree forbids removing during iteration.\n\t//\n\t// Dropped the moment the proposal closes: Vote refuses a closed proposal\n\t// before it looks here, so the tree would be rent on something nobody can\n\t// consult. The roll survives in the Voted events.\n\tvoted *bptree.BPTree\n}\n\n// entry is a kind and whether it may actually be proposed. Separate, because\n// publishing code is something a realm does and letting that code hold this\n// governor's authority is something the holders do. One registration call fuses\n// them, and then whoever may register can grant themselves any power with no\n// vote.\ntype entry struct {\n\tkind  Kind\n\trules Rules\n\tlive  bool\n}\n\n// maxLive bounds how many questions can be open at once. A security parameter,\n// not a storage one: it is the only thing between a determined proposer and a\n// list nobody can read.\n//\n// Cancel frees a slot outright. Time only makes one RECLAIMABLE — a read can\n// work out that a proposal is over but cannot write it down — so the slot\n// comes back when somebody calls Settle, or when sweep finds it on the next\n// Propose. Reading this as \"time frees a slot\" is how the reclaim came to be\n// something a proposer could hold shut; see the cursor sweep keeps.\n//\n// The open section of the front page has no length cap of its own, so its\n// worst case is this many rows at maxTitle and maxKindName each: 14,220 bytes,\n// which is a page. Ten thousand slots would be 2.2MB, which is not. A test\n// holds the product against a ceiling, so raising either number fails it.\nconst maxLive = 64\n\n// govLanes is how many of those slots only the governor's own kinds may take.\n//\n// Without it the sixty-four are a commons and any one kind may hold all of\n// them. Sixty-four proposals of some cheap kind with a long voting window, and\n// nothing else can be asked until that window runs out — including the\n// govern:rules vote that would shorten the window. The realm stays up and stops\n// being governable, which is the one failure it must not have, because every\n// other kind of congestion is something the holders can vote their way out of.\n//\n// Priced rather than assumed: the bootstrap terms put ProposeBps at zero, so\n// any holder may propose, and sixty-four proposals is about 56 GNOT of deposit\n// — refundable, and held for one bootstrap voting window of a week.\n//\n// Eight rather than one. The governor has five kinds of its own and they are\n// not alternatives: retuning a kind, adopting another, and retiring a third can\n// all be live questions at once, and a lane of one would mean the first of them\n// blocks the rest. Eight leaves room for a second round of each.\n//\n// This does NOT stop the governor's own kinds crowding each other out, and it\n// is not meant to. A realm whose holders have filled the governance lane with\n// governance is congested by its own doing and can wait; a realm shut out of\n// governance by a kind it adopted for something else cannot.\nconst govLanes = 8\n\n// maxOpen is what any other kind may fill, which is the number a proposer\n// actually meets. maxLive stays the bound on the front page, since the lane is\n// a reallocation of the sixty-four and not an addition to them.\nconst maxOpen = maxLive - govLanes\n\nvar (\n\terrNoSuchKind          = govErr(\"no kind by that name has been offered\")\n\terrAlreadyLive         = govErr(\"that kind is already adopted\")\n\terrNotLive             = govErr(\"that kind is not adopted\")\n\terrCannotRetireBuiltin = govErr(\"the governor's own kinds cannot be retired\")\n)\n\n// Governor is one governing body: which powers it has adopted, every question\n// it has been asked, and where its slot reclaim had got to.\n//\n// Allocated by the consuming realm, which is what makes this a library rather\n// than a realm. A /p/ package's own state is frozen after init, so nothing\n// durable could live at package level; the trees below carry the consumer's\n// storage stamp, and every method here borrows the consumer's authority back\n// for the write.\n//\n// Every field is unexported and no method hands one out. /p/-declared types can\n// be named by other /p/ packages, so an exported *proposal or a method\n// returning one would let a stranger declare a mutator over it — and the\n// storage-realm borrow would run that mutator under the CONSUMING realm's\n// authority. See gno-security-guide.md §3(B) and §4.\ntype Governor struct {\n\tvoters Electorate\n\ttoken  Token\n\n\tkinds     *bptree.BPTree // name -\u003e *entry\n\tproposals *bptree.BPTree // enc(id) -\u003e *proposal\n\topenIdx   *bptree.BPTree // digest -\u003e enc(id), the live ones\n\n\tpropSeq int64\n\n\t// executing is the re-entrancy latch: a kind calling back in while its own\n\t// Do runs would see a proposal mid-execution.\n\texecuting bool\n\n\t// sweepFrom is where the next slot reclaim starts scanning, and it rotates.\n\t// The index is keyed by digest(kind, payload) and a proposer chooses their\n\t// payload, so they choose their key: park a few long-running proposals on\n\t// the lowest keys and a FIXED window never sees past them, freeing nothing\n\t// while the rest of the list is finished business. Beating the lowest of\n\t// fifty-six random keys is a few hundred hashes offline.\n\tsweepFrom string\n}\n\n// reserved names the governor's own kinds. Offer refuses this prefix, so no\n// realm can publish something that renders as a built-in.\nconst reserved = \"govern:\"\n\n// isReserved reports whether a name belongs to the governor itself.\n//\n// One predicate, two callers: Offer refusing to publish under the prefix, and\n// govern:retire refusing to withdraw something carrying it. It was written\n// twice, and a weakened retire copy would let the holders withdraw\n// govern:adopt — after which no kind could ever be adopted again and no\n// proposal could restore it.\nfunc isReserved(name string) bool {\n\treturn len(name) \u003e= len(reserved) \u0026\u0026 name[:len(reserved)] == reserved\n}\n\n// NewRules builds the terms a kind would pass on. A realm cannot allocate\n// another realm's struct — govern.Rules{...} written elsewhere is refused with\n// \"cannot allocate ... in realm\" — so the value comes into existence here and\n// the caller passes numbers. Without it, Offer was uncallable from outside this\n// package.\nfunc NewRules(quorumBps, thresholdBps, votingBlocks, delayBlocks, graceBlocks, proposeBps int64) Rules {\n\treturn Rules{\n\t\tQuorumBps: quorumBps, ThresholdBps: thresholdBps,\n\t\tVotingBlocks: votingBlocks, DelayBlocks: delayBlocks,\n\t\tGraceBlocks: graceBlocks, ProposeBps: proposeBps,\n\t}\n}\n\n// Offer shelves a kind. It takes effect on nothing.\n//\n// Ungated, because an offered kind can do nothing. What it buys is a name\n// pointing at published, immutable code the holders are later asked to approve.\n//\n// The two-step exists because MsgCall.Args is []string: an account cannot hand\n// over a Kind, only a realm can construct one. If adopting required passing\n// the value, only realms could grow the governor — and that decision has to\n// belong to the holders. Offer is the realm's half; the vote is theirs.\nfunc (g *Governor) Offer(who address, k Kind, r Rules) {\n\tname := k.Name()\n\tmustBeUsableName(name)\n\tif isReserved(name) {\n\t\tpanic(\"govern: that prefix belongs to the governor\")\n\t}\n\tg.mustBeSaneRules(r)\n\n\t// A name, once offered, is bound to that code forever.\n\t//\n\t// The alternative — allowing a re-offer while the kind is not yet adopted —\n\t// is a live attack, not an untidiness. Offer harmless code, wait for an\n\t// adoption proposal to open on it, then re-offer under the same name while\n\t// the vote runs. The holders read the first Describe, approve it, and Do\n\t// dispatches to the second. gno's own daokit has this shape and records it\n\t// as a TODO; here the vote would be a signature on a blank cheque.\n\t//\n\t// Pinning the value into the proposal would also close it, at the cost of\n\t// a Kind stored per proposal and a rule about which copy wins. Refusing\n\t// the rewrite is smaller and matches how the chain already behaves: a\n\t// realm cannot be redeployed at its path either. A realm with new code\n\t// publishes a new name — treasury/spend/v2 — which the holders then have\n\t// to adopt knowingly, which is the entire point.\n\tif g.entryOf(name) != nil {\n\t\tpanic(\"govern: that name is taken; publish a new one rather than \" +\n\t\t\t\"rewriting what may already be under a vote\")\n\t}\n\tg.kinds.Set(name, \u0026entry{kind: k, rules: r})\n\n\t// The terms go out with it. A holder deciding whether to adopt this is\n\t// deciding on the code AND on the bar it would pass at, and the two are\n\t// bound together from here — Offer refuses to rewrite either.\n\tchain.Emit(kindOfferedEvent,\n\t\t\"name\", name,\n\t\t\"offerer\", who.String(),\n\t\t\"quorumBps\", strconv.FormatInt(r.QuorumBps, 10),\n\t\t\"thresholdBps\", strconv.FormatInt(r.ThresholdBps, 10),\n\t\t\"votingBlocks\", strconv.FormatInt(r.VotingBlocks, 10),\n\t\t\"delayBlocks\", strconv.FormatInt(r.DelayBlocks, 10),\n\t\t\"graceBlocks\", strconv.FormatInt(r.GraceBlocks, 10),\n\t\t\"proposeBps\", strconv.FormatInt(r.ProposeBps, 10),\n\t)\n}\n\n// maxTitle bounds the headline, which goes on the shared page. Every open\n// proposal's title is concatenated into Render(\"\"), and the only cap there is\n// on the decided tail — so the page every holder reads is as long as the open\n// titles put together. Anything needing more room is what the payload is for.\nconst maxTitle = 120\n\n// maxPayload bounds the argument. Looser than the title on purpose: this is\n// what the proposal actually says, a batch carries up to maxBatch members in\n// one, and the cost falls on the proposer — Render lists titles, so nobody\n// loads a payload without asking for that proposal by number.\nconst maxPayload = 4096\n\n// maxKindName bounds a name. Offer is ungated, so it is chosen by whoever\n// turns up, and everything that renders a proposal renders it.\nconst maxKindName = 64\n\n// mustBeUsableName refuses a name that cannot be used everywhere a name is\n// used.\n//\n// A kind's name is not only a registry key. It is the first field of a batch\n// member line, and parseBatch ends that field at the first SPACE; it goes into\n// the digest that decides whether two proposals are the same question; and it\n// is rendered on a page somebody has to read before voting. A name that works\n// in one of those and not the others is a kind that is quietly unusable.\n//\n// A space is the one that looks harmless and is not. \"my kind\" offers and\n// proposes perfectly well, and then a batch reads everything after the space\n// as the payload — so the member resolves to a kind called \"my\", the batch is\n// refused with \"no kind called my\", and nothing points at the name.\n//\n// Printable ASCII, no space. That is what every kind here already uses — fee,\n// treasury/spend, govern:adopt — and it is the largest set that works in all\n// three places at once.\nfunc mustBeUsableName(name string) {\n\tif name == \"\" {\n\t\tpanic(\"govern: a kind needs a name\")\n\t}\n\tif len(name) \u003e maxKindName {\n\t\tpanic(\"govern: that name is too long\")\n\t}\n\tfor i := 0; i \u003c len(name); i++ {\n\t\tif c := name[i]; c \u003c= ' ' || c \u003e '~' {\n\t\t\tpanic(\"govern: a kind name must be printable ASCII with no spaces\")\n\t\t}\n\t}\n}\n\n// mustBeUsableTitle bounds the one piece of free prose an ungated caller puts\n// on a page everybody reads.\n//\n// A title goes onto the front page inside a markdown list item, so a newline\n// ends that item and starts a row of the proposer's own — a forged proposal\n// with a state and a link they choose:\n//\n//   - [#1](:1) **x\n//   - [#999](:999) **Ratified by the foundation** — `govern:minter` · succeeded\n//\n// The bootstrap kinds ask for no stake, so that cost nothing but a transaction.\n//\n// Control characters, not printable-ASCII: a title is prose and keeps spaces\n// and any language. Markdown within a line is left alone — it cannot leave the\n// row it belongs to, and structure is the part a reader cannot check.\nfunc mustBeUsableTitle(title string) {\n\tif len(title) \u003e maxTitle {\n\t\tpanic(\"govern: that title is too long\")\n\t}\n\tif err := TextOnly(title); err != nil {\n\t\tpanic(\"govern: a title \" + err.Error())\n\t}\n}\n\n// TextOnly reports whether a string is text rather than page structure.\n//\n// Exported because it is a kind author's problem. A payload comes from whoever\n// proposes — any address at all when ProposeBps is zero — and Describe renders\n// it onto a page others vote from, so a raw payload hands page structure to a\n// stranger. The built-ins are safe by accident: their payloads are kind names\n// and addresses, which cannot hold a newline. It bites the first kind that\n// takes free text.\n//\n//\tfunc (k mykind) Check(payload string) error {\n//\t\tif len(payload) \u003e 280 {\n//\t\t\treturn errTooLong\n//\t\t}\n//\t\treturn govern.TextOnly(payload)\n//\t}\n//\n// Control characters, not printable-ASCII, so prose in any language survives —\n// every byte of a multi-byte character is \u003e= 0x80. Markdown within a line is\n// allowed: it cannot leave the row it belongs to.\n//\n// The governor cannot apply this to payloads itself; govern:batch is defined\n// in terms of newlines. Only a kind knows its own grammar.\nfunc TextOnly(s string) error {\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tif c := s[i]; c \u003c 0x20 || c == 0x7f {\n\t\t\treturn errNotTextOnly\n\t\t}\n\t}\n\treturn nil\n}\n\nvar errNotTextOnly = govErr(\"cannot contain control characters\")\n\n// saneRules is the one definition of terms this realm will accept — one\n// function, two callers, after two copies drifted apart.\n//\n// Both copies missed ProposeBps. Above a hundred percent it asks a proposer to\n// hold more than everything, so nobody can open a question under that kind:\n// adopted and unusable. And govern:rules accepted only five of the six terms,\n// so the one field where a bad value bricks a kind was the one nobody could\n// retune. It takes all six now.\nfunc saneRules(r Rules) error {\n\tif r.QuorumBps \u003c 0 || r.QuorumBps \u003e bps || r.ThresholdBps \u003c 0 || r.ThresholdBps \u003e bps {\n\t\treturn govErr(\"basis points out of range\")\n\t}\n\tif r.ProposeBps \u003c 0 || r.ProposeBps \u003e bps {\n\t\t// Exactly bps is allowed: \"only somebody holding the entire supply may\n\t\t// propose this\" is a coherent, if severe, thing to want.\n\t\treturn govErr(\"a proposer cannot be asked to hold more than the whole supply\")\n\t}\n\tif r.VotingBlocks \u003c= 0 {\n\t\treturn govErr(\"a vote needs a period\")\n\t}\n\tif r.GraceBlocks \u003c= 0 {\n\t\t// A decision has to be able to expire, because never expiring leaks a\n\t\t// slot.\n\t\t//\n\t\t// A Succeeded proposal keeps its place in the open index until it is\n\t\t// executed, because until then a duplicate would be a second live copy\n\t\t// of the same question. With no grace period there is nothing that ever\n\t\t// ends it: Cancel refuses a decided proposal, Settle and sweep find\n\t\t// nothing to change, and the slot is held for the life of the realm.\n\t\t// Sixty-four like that and nobody can open a proposal again.\n\t\t//\n\t\t// Refusing zero is what makes \"every proposal eventually gives its slot\n\t\t// back\" true by construction, and it agrees with what this realm\n\t\t// already says about grace: a decision nobody executed is a decision\n\t\t// about a world that has moved.\n\t\treturn govErr(\"a decision has to expire; a grace period of zero holds \" +\n\t\t\t\"its slot for the life of the realm\")\n\t}\n\tif r.DelayBlocks \u003c 0 {\n\t\t// GraceBlocks \u003c 0 needs no test here: the GraceBlocks \u003c= 0 return above\n\t\t// already refuses it. Negative behaves as zero everywhere it is used, so\n\t\t// this refuses a value that means nothing rather than one that does harm —\n\t\t// and stops it being stored and rendered as a term somebody agreed to.\n\t\treturn govErr(\"a period cannot be negative\")\n\t}\n\treturn nil\n}\n\nfunc (g *Governor) mustBeSaneRules(r Rules) {\n\tif err := saneRules(r); err != nil {\n\t\tpanic(\"govern: \" + err.Error())\n\t}\n}\n\n// init makes the governor able to govern itself, and nothing else. These are\n// live from the first block because adopting a kind is itself a kind, so there\n// is nothing to bootstrap them with. Everything beyond must be offered and\n// adopted, so not even the deployer can add a power without a vote.\n\n// BootstrapRules are the terms the governor governs ITSELF on.\n//\n// Exported because a consuming realm registering a power of its own —\n// the mint, most of all — should govern it on the same terms rather than\n// inventing a second set that can drift from these.\nfunc BootstrapRules() Rules {\n\treturn Rules{\n\t\t// A fifth of the supply has to turn out. Low enough to reach with\n\t\t// dispersed holders, high enough that a handful cannot decide alone\n\t\t// while everybody else is asleep.\n\t\tQuorumBps: 2000,\n\n\t\t// Two thirds of what was cast, not half. A simple majority is the\n\t\t// wrong bar for changing the rules of the game — these kinds decide\n\t\t// who may mint and what powers exist — and 6600 is exact where gno's\n\t\t// own govdao carries 66.66 as a float64 and compares with \u003e=.\n\t\tThresholdBps: 6600,\n\n\t\t// A week to vote, so holders in any timezone get a turn. It is a\n\t\t// DEADLINE and not a duration: a vote nothing further could change\n\t\t// closes on the arithmetic, so an agreed decision does not wait it out.\n\t\tVotingBlocks: 7 * 24 * 60 * 60 / 5,\n\n\t\t// Two days between deciding and doing, which is what a timelock is\n\t\t// for: time for somebody who dislikes the outcome to sell, leave, or\n\t\t// argue before it takes effect.\n\t\tDelayBlocks: 2 * 24 * 60 * 60 / 5,\n\n\t\t// A fortnight to execute, and then the decision expires. A proposal\n\t\t// nobody executed for two weeks is a decision about a world that has\n\t\t// moved, and gno's own govdao had to bolt on a reject path precisely\n\t\t// because proposals could otherwise sit forever.\n\t\tGraceBlocks: 14 * 24 * 60 * 60 / 5,\n\t}\n}\n\n// installBuiltins registers the kinds the governor governs itself with.\nfunc (g *Governor) installBuiltins() {\n\t// The terms the governor governs ITSELF on. See BootstrapRules.\n\tbootstrap := BootstrapRules()\n\tg.kinds.Set(adoptKind{}.Name(), \u0026entry{kind: adoptKind{}, rules: bootstrap, live: true})\n\tg.kinds.Set(retireKind{}.Name(), \u0026entry{kind: retireKind{}, rules: bootstrap, live: true})\n\tg.kinds.Set(rulesKind{}.Name(), \u0026entry{kind: rulesKind{}, rules: bootstrap, live: true})\n\tg.kinds.Set(batchKind{}.Name(), \u0026entry{kind: batchKind{}, rules: bootstrap, live: true})\n}\n\n// adoptKind turns an offered kind live. Its payload is only a name, so what\n// the holders read is exactly what they are approving.\ntype adoptKind struct{}\n\nfunc (k adoptKind) Name() string { return reserved + \"adopt\" }\n\nfunc (k adoptKind) describe(g *Governor, payload string) string {\n\tout := \"adopt the kind `\" + payload + \"`, letting it be proposed and executed\"\n\te := g.entryOf(payload)\n\tif e == nil {\n\t\treturn out + \"\\n\\n_Nothing has been offered under that name._\"\n\t}\n\t// The terms, not just the name: a holder approving a power needs to see how\n\t// easily it can be used. A hostile realm offers a spending kind with a\n\t// hundredth of a percent quorum and a one-block vote, and the adoption\n\t// reads exactly like an honest one.\n\t//\n\t// Still a total function of the payload — an offered name is bound to its\n\t// code permanently, and govern:rules refuses to retune an unadopted kind,\n\t// so none of this can move between proposing and executing.\n\tout += ufmt.Sprintf(\"\\n\\nIt would pass on:\"+\n\t\t\"\\n- quorum: %s of the supply\"+\n\t\t\"\\n- threshold: %s of votes cast\"+\n\t\t\"\\n- time to vote: %s\"+\n\t\t\"\\n- delay before it can run: %s\"+\n\t\t\"\\n- expires after: %s\",\n\t\tpct(e.rules.QuorumBps), pct(e.rules.ThresholdBps),\n\t\tspan(e.rules.VotingBlocks), span(e.rules.DelayBlocks), span(e.rules.GraceBlocks))\n\t// Always rendered, including when it is zero — which is when it matters\n\t// most. Zero means Propose skips the stake check, so any address may open\n\t// a proposal holding nothing, and omitting the line read as \"not\n\t// applicable\" rather than \"anybody\". A permissive default is the term a\n\t// page most needs to say out loud.\n\tout += \"\\n- who may propose: \" + proposerBar(e.rules.ProposeBps)\n\treturn out\n}\n\nfunc (k adoptKind) check(g *Governor, payload string) error {\n\te := g.entryOf(payload)\n\tif e == nil {\n\t\treturn errNoSuchKind\n\t}\n\tif e.live {\n\t\treturn errAlreadyLive\n\t}\n\treturn nil\n}\n\nfunc (k adoptKind) run(g *Governor, dispatch Dispatch, payload string) error {\n\te := g.entryOf(payload)\n\tif e == nil {\n\t\treturn errNoSuchKind\n\t}\n\te.live = true\n\treturn nil\n}\n\n// retireKind withdraws one. Offered-but-not-live is the resting state, so a\n// retired kind can be adopted again without the realm re-offering it.\ntype retireKind struct{}\n\nfunc (k retireKind) Name() string { return reserved + \"retire\" }\n\nfunc (k retireKind) describe(g *Governor, payload string) string {\n\treturn \"retire the kind `\" + payload + \"`, so it can no longer be proposed\" +\n\t\t\"\\n\\nThis also kills any proposal of that kind that has already been \" +\n\t\t\"decided and not yet run: Execute refuses a kind that is no longer \" +\n\t\t\"adopted, and records the proposal as failed. Retiring is not only \" +\n\t\t\"about the future.\"\n}\n\nfunc (k retireKind) check(g *Governor, payload string) error {\n\te := g.entryOf(payload)\n\tif e == nil {\n\t\treturn errNoSuchKind\n\t}\n\tif !e.live {\n\t\treturn errNotLive\n\t}\n\tif isReserved(payload) {\n\t\treturn errCannotRetireBuiltin\n\t}\n\treturn nil\n}\n\nfunc (k retireKind) run(g *Governor, dispatch Dispatch, payload string) error {\n\te := g.entryOf(payload)\n\tif e == nil {\n\t\treturn errNoSuchKind\n\t}\n\te.live = false\n\treturn nil\n}\n\n// Propose opens a question. One maketx call, two strings — which is the whole\n// design and the reason the payload is not a struct.\n//\n// The quorum is the rules' QuorumBps fraction of the engaged weight. A consumer\n// that computes its own absolute bar — a court sizing quorum to a claim's open\n// interest — uses ProposeWithQuorum instead.\nfunc (g *Governor) Propose(who address, kind, payload, title string) int64 {\n\treturn g.propose(who, kind, payload, title, 0)\n}\n\n// ProposeWithQuorum is Propose with an ABSOLUTE turnout the question needs,\n// replacing the rules' QuorumBps fraction for this one proposal. The figure is\n// the consumer's to compute (a court's max(5% supply, min(1×X̄, ⅓ votable))) and\n// is snapshotted here, so the bar cannot move under an open vote. A non-positive\n// floor is refused — a caller wanting the bps formula calls Propose.\n//\n// Additive: Propose is unchanged and passes zero, which every existing user does.\nfunc (g *Governor) ProposeWithQuorum(who address, kind, payload, title string, quorumFloor int64) int64 {\n\tif quorumFloor \u003c= 0 {\n\t\tpanic(\"govern: a quorum floor has to be positive; use Propose for the bps bar\")\n\t}\n\treturn g.propose(who, kind, payload, title, quorumFloor)\n}\n\nfunc (g *Governor) propose(who address, kind, payload, title string, quorumFloor int64) int64 {\n\tif g.executing {\n\t\tpanic(\"govern: cannot propose from inside an execution\")\n\t}\n\t// Checked before anything else is done with them: these are the two\n\t// strings an ungated caller supplies, and they are persisted for the life\n\t// of the realm.\n\tmustBeUsableTitle(title)\n\tif len(payload) \u003e maxPayload {\n\t\tpanic(\"govern: that payload is too long\")\n\t}\n\tk := g.kindOf(kind)\n\tif err := g.checkKind(k, payload); err != nil {\n\t\tpanic(\"govern: \" + err.Error())\n\t}\n\n\t// The electorate is the checkpointed supply at the last sealed epoch. Not a\n\t// hand-kept roll: the token already remembers who held what, and asking it is\n\t// what makes the weight historical.\n\t//\n\t// THIS ALONE DOES NOT MAKE WEIGHT UNRENTABLE, and an earlier version of this\n\t// comment claimed it did. The anchor is derived HERE, at propose time, by\n\t// whoever proposes — so a renter buys float, waits ONE epoch for the\n\t// checkpoint to seal, proposes (pinning the anchor to a window in which they\n\t// hold), sells, and votes with weight they no longer own. Measured end to end:\n\t// 300 B of pinned weight cast at 600x a quorum floor with a live balance of\n\t// zero and one epoch of capital at risk. What this line buys on its own is\n\t// only that weight cannot be acquired AFTER the question exists.\n\t//\n\t// CLOSED, and not here — the other half is supplied by the CONSUMER, through\n\t// VoteWithCap, as a ceiling of the voter's live balance. The engine still\n\t// derives this snapshot and takes the lesser, so a consumer can only lower.\n\t// See VOTEFLOOR.md; kourtv2 caps all three of its lanes and r/govern does not,\n\t// which is why the exposure is described here rather than fixed here.\n\tat := g.voters.Epoch() - 1\n\tif at == 0 {\n\t\tpanic(\"govern: no sealed epoch yet — the chain is too young to vote\")\n\t}\n\ttotal := g.voters.PastTotal(at)\n\tif total \u003c= 0 {\n\t\tpanic(\"govern: nothing was in issue at that epoch\")\n\t}\n\tif total \u003e maxWeighable {\n\t\t// The ceiling the tally silently assumed. grc20votes caps here so it never\n\t\t// trips; a swapped-in electorate that does not would overflow yes*bps into\n\t\t// a negative tally — a won vote reported as lost. Refused at the door,\n\t\t// where it reads as a misconfigured electorate rather than the governor\n\t\t// miscounting.\n\t\tpanic(\"govern: the snapshot supply exceeds what a tally can weigh\")\n\t}\n\t// Snapshotted with the rest, so a bar cannot move under an open vote.\n\tengaged := g.voters.EngagedTotal(at)\n\tif engaged \u003c= 0 {\n\t\t// An electorate that engages nobody makes every quorum TRIVIAL, which is\n\t\t// the opposite of what this comment said for a long time and the reason\n\t\t// the guard is load-bearing rather than tidy. The bar is\n\t\t// `cast*bps \u003e= engaged*QuorumBps`; at engaged == 0 the right-hand side is\n\t\t// zero and the comparison holds for any turnout, including none — so\n\t\t// quorum stops existing, and at the deadline one yes vote carries the\n\t\t// question however few showed up.\n\t\t//\n\t\t// Reachable with an honest electorate rather than a broken one:\n\t\t// EngagedTotal is there so a realm can drop idle weight out of the\n\t\t// denominator, and one where all weight is idle returns zero truthfully.\n\t\tpanic(\"govern: the engaged weight has to be positive\")\n\t}\n\tif engaged \u003e total {\n\t\t// Clamp rather than refuse. A court deliberately hosts claims too big for\n\t\t// its electorate to decide, and an EngagedTotal above the snapshot supply\n\t\t// should make the bar unreachable-but-valid, not abort the proposal.\n\t\t// Clamping to total keeps rest = total - cast non-negative in the tally.\n\t\t// The token this ships with returns engaged == total, so this never fires\n\t\t// for it — it is here for a replacement electorate.\n\t\tengaged = total\n\t}\n\n\trules := g.rulesOf(kind)\n\tif rules.ProposeBps \u003e 0 {\n\t\t// The numbers, not just the verdict. The realm knows the bar and what\n\t\t// the caller held; withholding both leaves somebody to work out from\n\t\t// the terms page what they are short of, and the epoch matters most of\n\t\t// all — a holder refused here may well hold plenty NOW and nothing at\n\t\t// the sealed epoch this is weighed at, which reads as a bug rather than\n\t\t// as the anti-flash-loan rule doing its job.\n\t\tif held := g.voters.PastVotes(who, at); held*bps \u003c total*rules.ProposeBps {\n\t\t\tpanic(ufmt.Sprintf(\"govern: not enough voting power to open this: \"+\n\t\t\t\t\"%s of the supply is required, and you held %d of %d at epoch %d\",\n\t\t\t\tpct(rules.ProposeBps), held, total, at))\n\t\t}\n\t}\n\t// Everything may fill the board except the last few slots, which the\n\t// governor's own kinds keep — see govLanes.\n\tlimit := maxLive\n\tif !isReserved(kind) {\n\t\tlimit = maxOpen\n\t}\n\tif g.openIdx.Size() \u003e= limit {\n\t\t// Try to reclaim before refusing: a full list is usually full of\n\t\t// finished business nobody has written down.\n\t\tg.sweep()\n\t}\n\tif g.openIdx.Size() \u003e= limit {\n\t\tif limit \u003c maxLive {\n\t\t\tpanic(\"govern: too many proposals are already open; the last \" +\n\t\t\t\t\"few slots are kept for the governor's own kinds\")\n\t\t}\n\t\tpanic(\"govern: too many proposals are already open\")\n\t}\n\td := g.digest(kind, payload)\n\tif v := g.openIdx.Get(d); v != nil {\n\t\t// Settle the one already standing here before refusing on its account.\n\t\t//\n\t\t// The index holds finished proposals too — a read can work out that a\n\t\t// proposal lost but cannot write it down, and the sweep above only\n\t\t// runs when the list is FULL. So without this, a question defeated on\n\t\t// its deadline and left unrecorded blocks the same question from being\n\t\t// asked again, and the refusal says \"already open\" about something\n\t\t// that closed.\n\t\t//\n\t\t// Targeted rather than a scan: the one proposal actually in the way,\n\t\t// on the rare path where a digest collides at all.\n\t\tif pv := g.proposals.Get(v.(string)); pv != nil {\n\t\t\tg.settle(pv.(*proposal))\n\t\t}\n\t}\n\tif g.openIdx.Has(d) {\n\t\tpanic(\"govern: an identical proposal is already open\")\n\t}\n\tnow := g.voters.Height()\n\tg.propSeq++\n\tp := \u0026proposal{\n\t\tid: g.propSeq, kind: kind, payload: payload, title: title, proposer: who,\n\t\trules: rules, epoch: at, total: total, engaged: engaged,\n\t\tquorumFloor: quorumFloor,\n\t\topened:      now, closes: now + rules.VotingBlocks,\n\t\topenedTime:  g.voters.Now(),\n\t\tclosesTime:  g.voters.Now() + rules.VotingBlocks*secsPerBlock,\n\t\t// The voter roll is bought here, by the proposer, including its first\n\t\t// node — see rollSentinel just below the Set. Whoever asks, pays.\n\t\t//\n\t\t// The alternative is to let the first voter buy it, on the argument\n\t\t// that a proposal nobody answers should cost nothing to leave\n\t\t// unanswered. That saving only ever accrues to a proposal that failed,\n\t\t// and it charges the first voter eleven times what the second pays.\n\t\t// Being early is the wrong thing to tax.\n\t\t//\n\t\t// Fanout 32 on purpose. A narrower tree makes that first node cheaper\n\t\t// (2,699 bytes at fanout 8 against 4,955) until the ninth voter, where\n\t\t// the tree splits and that vote costs 5,476 — thirteen times what the\n\t\t// eighth paid, for arriving in the wrong order. See\n\t\t// docs/DESIGN.md.\n\t\tstate: stateActive, voted: bptree.NewBPTree32(),\n\t}\n\tp.voted.Set(rollSentinel, ballot(0))\n\tg.proposals.Set(enc(uint64(g.propSeq)), p)\n\tg.openIdx.Set(d, enc(uint64(g.propSeq)))\n\n\tchain.Emit(proposalOpenedEvent,\n\t\t\"id\", strconv.FormatInt(g.propSeq, 10),\n\t\t\"kind\", kind,\n\t\t\"proposer\", who.String(),\n\t\t\"epoch\", strconv.FormatUint(uint64(at), 10),\n\t)\n\treturn g.propSeq\n}\n\n// Vote records a choice, weighed at the proposal's snapshot.\nfunc (g *Governor) Vote(who address, id int64, choice string) {\n\tg.castVote(who, id, choice, \"\", 0)\n}\n\n// VoteWithCap is Vote where the consumer supplies a CEILING on the weight, and\n// the engine still derives the weight itself and takes the lesser.\n//\n// A CEILING, NEVER A WEIGHT, and the distinction is the whole reason this method\n// is shaped like this. An earlier attempt added VoteWithWeight, which took the\n// figure to tally. That was a permissionless verdict flip.\n//\n// A supplied weight is not drawn from p.total, so `cast` could exceed it and\n// `rest := p.total - cast` went NEGATIVE. Written out, the two early arms are\n//\n//\tearly-succeed  yes*bps                    \u003e= (total - abstain)*T\n//\tearly-defeat   (total - no - abstain)*bps  \u003c (total - abstain)*T\n//\n// because turnout is yes+no+abstain, so yes+no+rest is identically total-abstain.\n// The damage was therefore an inflated NUMERATOR against a snapshotted denominator:\n// `yes` could exceed anything the electorate held while (total - abstain) stayed\n// put, passing on support that did not exist. And a negative rest shrinks\n// (yes+rest), so the defeat arm could fire on a question still open.\n//\n// AN EARLIER VERSION OF THIS COMMENT SAID the arm \"reduced to yes*bps \u003e= (total -\n// abstain)*T, dropping `no` out of the test entirely\". The identity above shows\n// that reduction is not a symptom of anything: it holds always, and `no` is not in\n// the early-succeed comparison and never was. The description was repeated in four\n// files before the algebra was checked, which is what\n// TestTheEarlyArmsIgnoreNoAndACapOnlyDelays now pins. Here the consumer can only\n// LOWER what this package read for itself, so\n//\n//\tΣ w  ≤  Σ PastVotes(·, p.epoch)  ≤  PastTotal(p.epoch)  =  p.total\n//\n// holds as an inequality rather than as a promise a caller has to keep. A hostile\n// consumer cannot raise `cast`, and the contract electorate.gno says this engine\n// \"cannot check and cannot recover from\" stays enforced where it lives.\n//\n// NO SENTINEL. `cap \u003c= 0` is refused, and Vote is the uncapped path. Treating zero\n// as \"uncapped\" was tried in the plan for this change and reopened the exploit\n// verbatim: a renter who has sold everything HAS a floor of zero, so zero is not an\n// edge case, it is the attack's terminal state.\n//\n// Why a consumer would want this: kourtv2 caps at the voter's live balance, so\n// weight that has been sold back cannot vote. See VOTEFLOOR.md.\nfunc (g *Governor) VoteWithCap(who address, id int64, choice string, cap int64) {\n\tif cap \u003c= 0 {\n\t\tpanic(\"govern: a vote cap must be positive; use Vote for no cap\")\n\t}\n\tg.castVote(who, id, choice, \"\", cap)\n}\n\n// VoteWithReason is Vote with a note the voter wants on the record — the same\n// pair OpenZeppelin has. Two entrypoints rather than one optional argument\n// because MsgCall carries a fixed list of strings and gno has no optional\n// parameters.\n//\n// The reason is emitted and never stored: it rides the Voted event, so the\n// voter pays gas for the bytes and the ledger carries none of them. Storing it\n// would be a new key per vote, on the path this design keeps cheapest.\n//\n// Bounded by maxReason. Not filtered through TextOnly, unlike a title: the\n// realm never renders this on a page, so a consumer that displays it is\n// responsible for its own escaping, as with any event field.\nfunc (g *Governor) VoteWithReason(who address, id int64, choice, reason string) {\n\tif len(reason) \u003e maxReason {\n\t\tpanic(\"govern: that reason is too long\")\n\t}\n\tg.castVote(who, id, choice, reason, 0)\n}\n\nfunc (g *Governor) castVote(who address, id int64, choice, reason string, cap int64) {\n\tif g.executing {\n\t\tpanic(\"govern: cannot vote from inside an execution\")\n\t}\n\tp := g.mustProposal(id)\n\tg.settle(p)\n\tif p.state != stateActive {\n\t\tpanic(\"govern: that proposal is closed\")\n\t}\n\tif p.voted.Has(string(who)) {\n\t\tpanic(\"govern: already voted\")\n\t}\n\t// Weighed as of the snapshot, so buying in after the question was asked buys\n\t// nothing, and selling out afterwards costs nothing.\n\t//\n\t// THE SECOND HALF IS A KNOWN, MEASURED EXPOSURE and it is still not closed\n\t// HERE — it is closed by the caller, if the caller chooses to. VoteWithCap\n\t// takes a ceiling and the clamp below applies it, so a consumer that passes\n\t// the voter's live balance gets min(snapshot, held) and the rental dies;\n\t// plain Vote leaves the exposure exactly as propose() describes it. kourtv2\n\t// caps all three of its lanes; r/govern does not. VOTEFLOOR.md has the\n\t// derivation and why both halves are needed.\n\t//\n\t// A 29-line block describing a LIVE-BALANCE CAP stood here and was wrong twice\n\t// over: the cap was implemented, reverted in the same commit, and the prose\n\t// outlived it — in a commit whose subject was \"correct a false safety claim\".\n\t// It also argued against a design this file then shipped. Recorded because the\n\t// lesson is the expensive one: a comment describing a reverted mechanism is a\n\t// false safety claim with a plausible pedigree.\n\tw := g.voters.PastVotes(who, p.epoch)\n\tif w \u003c= 0 {\n\t\tpanic(\"govern: no voting power at that epoch\")\n\t}\n\t// The consumer's ceiling, and it may only LOWER. Written as an explicit clamp\n\t// rather than a min() helper so that the one property this block exists for —\n\t// w is never raised — is a single readable line that a guard can pin.\n\tif cap \u003e 0 \u0026\u0026 cap \u003c w {\n\t\tw = cap\n\t}\n\tswitch choice {\n\tcase \"yes\":\n\t\tp.yes += w\n\tcase \"no\":\n\t\tp.no += w\n\tcase \"abstain\":\n\t\t// Turnout, not indifference. Showing up to decline is a different\n\t\t// statement from silence, and it counts towards quorum while staying\n\t\t// out of the threshold.\n\t\tp.abstain += w\n\tdefault:\n\t\tpanic(\"govern: choice must be yes, no or abstain\")\n\t}\n\t// For a long time nothing read this — only Has() was ever asked of the\n\t// tree — and it was kept anyway, on the measurement that swapping the whole\n\t// value for a bool saves ONE byte per vote, because what a vote costs is\n\t// the address key and the tree's own entry overhead rather than the value\n\t// hanging off it. VoteOf reads it now, which is what the byte was for.\n\tp.voted.Set(string(who), packBallot(choice, w))\n\tg.settle(p)\n\n\t// The reason rides along only when there is one. An empty attribute on\n\t// every vote would cost every voter gas to say nothing, and would leave an\n\t// indexer unable to tell a voter who declined to explain from one whose\n\t// client cannot ask.\n\tif reason == \"\" {\n\t\tchain.Emit(votedEvent,\n\t\t\t\"id\", strconv.FormatInt(id, 10),\n\t\t\t\"voter\", who.String(),\n\t\t\t\"choice\", choice,\n\t\t\t\"weight\", strconv.FormatInt(w, 10),\n\t\t)\n\t\treturn\n\t}\n\tchain.Emit(votedEvent,\n\t\t\"id\", strconv.FormatInt(id, 10),\n\t\t\"voter\", who.String(),\n\t\t\"choice\", choice,\n\t\t\"weight\", strconv.FormatInt(w, 10),\n\t\t\"reason\", reason,\n\t)\n}\n\n// ballot is one vote as cast. The weight is what the tally used; the choice is\n// here so a kind can pay the side that voted with it, which is the whole reason\n// VoteOf exists — a scheme that pays only the winners gives a juror expecting to\n// lose no reason to turn up, which suppresses the honest side during exactly\n// the manipulation a vote is meant to stop.\n// A ballot is one int64, not a struct.\n//\n// The tree hands out each value as its own object for lazy loading, and a\n// struct there is a SECOND object per vote — measured at 846 bytes a vote\n// against 454 for a scalar. A vote is the one thing in this realm that happens\n// thousands of times, so 405 bytes of object header is the wrong place to spend\n// on tidiness.\n//\n// weight*4 + the choice. Two bits for three choices, and the weight is bounded\n// by maxSupply, which is MaxInt64/10000 — so the shift cannot overflow with\n// four orders of magnitude to spare.\ntype ballot int64\n\nconst (\n\tballotYes int64 = iota\n\tballotNo\n\tballotAbstain\n)\n\nfunc packBallot(choice string, weight int64) ballot {\n\tswitch choice {\n\tcase \"yes\":\n\t\treturn ballot(weight\u003c\u003c2 | ballotYes)\n\tcase \"no\":\n\t\treturn ballot(weight\u003c\u003c2 | ballotNo)\n\tcase \"abstain\":\n\t\treturn ballot(weight\u003c\u003c2 | ballotAbstain)\n\t}\n\t// Unreachable: castVote refuses anything else before it gets here. Panics\n\t// rather than defaulting, because a silent default would file a vote under\n\t// a choice nobody made.\n\tpanic(\"govern: not a choice: \" + choice)\n}\n\nfunc (b ballot) unpack() (choice string, weight int64) {\n\tswitch int64(b) \u0026 3 {\n\tcase ballotYes:\n\t\tchoice = \"yes\"\n\tcase ballotNo:\n\t\tchoice = \"no\"\n\tdefault:\n\t\tchoice = \"abstain\"\n\t}\n\treturn choice, int64(b) \u003e\u003e 2\n}\n\n// rollSentinel is the key Propose writes into a new voter roll, so that the\n// roll's first node is bought by the person asking the question rather than by\n// whoever happens to answer it first.\n//\n// A bptree allocates nothing until its first key, and its leaf carries\n// fanout-sized backing arrays, so that first key costs about eleven times an\n// ordinary insert. Left to the first voter, that is a tax on being early —\n// which is the worst moment to put one, since a proposal with no votes yet is\n// the one that needs the first.\n//\n// The empty string, because it is the one key no address can be: an address is\n// bech32 and bech32 is never empty. Readers taking an address from outside are\n// still told to refuse it, since \"\" arrives as a valid Go string from anybody\n// who wants to ask.\nconst rollSentinel = \"\"\n\n// VoteOf is how an address voted, for a kind that has to pay them.\n//\n// One address at a time, on purpose. A list of every voter is an unbounded\n// return, and handing back the tree itself would be a live mutator holding this\n// realm's authority — so payouts are pull-based: each claimant asks about\n// themselves.\n//\n// Answers only while the roll survives. It is dropped by ReleaseRoll, so a kind\n// that pays out should either do it inside Do, where the roll is certainly\n// intact, or tell its claimants that the window closes when somebody reclaims\n// the deposit.\nfunc (g *Governor) VoteOf(id int64, who address) (choice string, weight int64, ok bool) {\n\tp := g.mustProposal(id)\n\tif p.voted == nil || who == rollSentinel {\n\t\treturn \"\", 0, false\n\t}\n\tv := p.voted.Get(string(who))\n\tif v == nil {\n\t\treturn \"\", 0, false\n\t}\n\tchoice, weight = v.(ballot).unpack()\n\treturn choice, weight, true\n}\n\n// wouldBe is what the rules and the clock say a proposal's state is, without\n// writing anything down.\n//\n// Split out from settle because reads must not mutate. Render walks every\n// proposal, and a settle-on-read there is an unbounded write hiding inside a\n// function that looks like a query — discarded harmlessly when it IS a query,\n// and a surprise when some realm calls Render inside a transaction.\n//\n// Every multiplication here is safe because the supply is capped at maxSupply.\n// yes, no and abstain are each bounded by the snapshot total, and total*bps\n// fits in int64 by construction.\nfunc (g *Governor) wouldBe(p *proposal, now int64) (int8, string) {\n\tswitch p.state {\n\tcase stateActive:\n\t\tcast := turnout(p)\n\t\t// The bar: an absolute floor the consumer set for this proposal, or the\n\t\t// rules' QuorumBps fraction of the engaged weight when it did not.\n\t\tvar quorum bool\n\t\tif p.quorumFloor \u003e 0 {\n\t\t\tquorum = cast \u003e= p.quorumFloor\n\t\t} else {\n\t\t\tquorum = cast*bps \u003e= p.engaged*p.rules.QuorumBps\n\t\t}\n\t\t// Every vote not yet cast, all of which could still say no. Negative if\n\t\t// an electorate's parts exceed its whole, which this token cannot do\n\t\t// and a replacement might; a clamp here changed no outcome, because a\n\t\t// broken roll settles on the first vote either way. The contract is on\n\t\t// the electorate instead, where somebody swapping one will read it.\n\t\trest := p.total - cast\n\t\t// p.yes \u003e 0 is not redundant, and leaving it out was a real hole.\n\t\t//\n\t\t// The threshold compares yes against yes+no. When everybody ABSTAINS\n\t\t// both are zero, the comparison is 0 \u003e= 0, and the proposal passed —\n\t\t// immediately, with nobody in favour, on the strength of an empty\n\t\t// denominator. Abstain is documented as turnout without support and it\n\t\t// was carrying proposals.\n\t\t//\n\t\t// Nothing passes without somebody voting for it, whatever the\n\t\t// threshold is set to.\n\t\tif quorum \u0026\u0026 p.yes \u003e 0 \u0026\u0026 p.yes*bps \u003e= (p.yes+p.no+rest)*p.rules.ThresholdBps {\n\t\t\t// Decided even if every remaining vote goes against. Waiting out\n\t\t\t// the clock on a settled question is latency, not deliberation —\n\t\t\t// and it is only computable because the denominator was\n\t\t\t// snapshotted, which is a third reason to snapshot.\n\t\t\treturn stateSucceeded, \"\"\n\t\t}\n\t\t// And the same argument the other way, which was missing.\n\t\t//\n\t\t// If the threshold cannot be reached even with every remaining vote in\n\t\t// favour, the question is as settled as one that has already won — and\n\t\t// this side is the more common one, because a proposal that is going\n\t\t// to lose usually loses by everybody ignoring it.\n\t\t//\n\t\t// It was asymmetric for no reason anybody could have defended: a\n\t\t// decided YES closed at once while a decided NO sat out its deadline,\n\t\t// holding a slot in a list bounded at maxLive, asking a question that\n\t\t// had already been answered.\n\t\tif p.yes+rest == 0 {\n\t\t\t// Nobody in favour and nobody left who could be. Settled, whatever\n\t\t\t// the clock says — this is the all-abstained case, which now loses\n\t\t\t// at once instead of sitting out its deadline having already\n\t\t\t// failed.\n\t\t\treturn stateDefeated, \"nobody voted in favour\"\n\t\t}\n\t\tif (p.yes+rest)*bps \u003c (p.yes+rest+p.no)*p.rules.ThresholdBps {\n\t\t\treturn stateDefeated, \"threshold can no longer be reached\"\n\t\t}\n\t\tif g.votingClosed(p, now) {\n\t\t\tif quorum \u0026\u0026 p.yes \u003e 0 \u0026\u0026 p.yes*bps \u003e= forAndAgainst(p)*p.rules.ThresholdBps {\n\t\t\t\treturn stateSucceeded, \"\"\n\t\t\t}\n\t\t\tif !quorum {\n\t\t\t\treturn stateDefeated, \"quorum not reached\"\n\t\t\t}\n\t\t\treturn stateDefeated, \"threshold not reached\"\n\t\t}\n\t\treturn stateActive, p.reason\n\n\tcase stateSucceeded:\n\t\tif g.expired(p) {\n\t\t\t// A proposal nobody executed for a long time is a decision about a\n\t\t\t// world that has moved. gno's own govdao has no deadline at all\n\t\t\t// and had to bolt on a reject path because proposals could sit\n\t\t\t// forever.\n\t\t\treturn stateExpired, \"not executed within the grace period\"\n\t\t}\n\t}\n\treturn p.state, p.reason\n}\n\n// settle writes down what wouldBe worked out. Only ever called from a\n// transaction, because only a transaction can record anything.\nfunc (g *Governor) settle(p *proposal) {\n\tnow := g.voters.Height()\n\tnext, reason := g.wouldBe(p, now)\n\tif next == p.state {\n\t\treturn\n\t}\n\tg.setState(p, next, reason)\n\tif next == stateSucceeded {\n\t\t// The delay runs from when the outcome is WRITTEN DOWN, not from when\n\t\t// the votes stopped mattering. Those differ for a proposal that wins on\n\t\t// its deadline: nobody has to be present when a deadline passes, so the\n\t\t// outcome is only recorded by the next transaction.\n\t\t//\n\t\t// Right way round — a timelock exists so people can react, and there is\n\t\t// nothing to react to until the decision is announced. Settle is\n\t\t// permissionless so announcing is never a privileged position.\n\t\tp.ready = now + p.rules.DelayBlocks\n\t\tp.readyTime = g.voters.Now() + p.rules.DelayBlocks*secsPerBlock\n\t\treturn\n\t}\n}\n\n// Execute runs a passed proposal. Permissionless on purpose: after the vote\n// and the delay there is nothing left to decide, so there is nobody left to\n// trust with the decision.\n// Dispatch runs an adopted kind. The consuming realm supplies it, because it is\n// the only thing that can: minting the sub-realm token a kind is handed needs a\n// live `cur`, and a /p/ package has none.\n//\n// It receives the kind, the sub-path to mint under, and the payload — and\n// NOTHING ELSE. No pointer into governor or ledger state passes through here,\n// which is what makes dispatching third-party code safe: gno-security-guide.md\n// §3(C) is about a victim invoking a caller-supplied value while holding its\n// own authority, and the damage in that class is done through a pointer\n// parameter. There is none to give.\n//\n// The realm's implementation is one line:\n//\n//\tfunc(k governor.Kind, sub, payload string) error {\n//\t\treturn k.Do(0, cur.Sub(sub), payload)\n//\t}\ntype Dispatch func(k Kind, subpath, payload string) error\n\nfunc (g *Governor) Execute(who address, id int64, run Dispatch) {\n\tif run == nil {\n\t\t// The engine holds no capability of its own, so without a dispatcher\n\t\t// there is nothing that could run a kind. Refused before anything is\n\t\t// written, so a caller who forgot one can supply it and retry.\n\t\tpanic(\"govern: a dispatcher is required to run a kind\")\n\t}\n\tif g.executing {\n\t\tpanic(\"govern: re-entrant execution\")\n\t}\n\tp := g.mustProposal(id)\n\tg.settle(p)\n\tif p.state != stateSucceeded {\n\t\tpanic(\"govern: that proposal is not waiting to be executed\")\n\t}\n\tif g.inDelay(p) {\n\t\t// Says when, because \"not yet\" without a when is an invitation to poll.\n\t\t// The proposal page has carried this figure all along; the refusal a\n\t\t// caller actually receives did not.\n\t\t//\n\t\t// In the unit that GOVERNS: a caller told \"1,000 blocks away\" has to know\n\t\t// the chain's pace to turn that into \"come back tomorrow\", and the pace\n\t\t// is what this conversion exists to stop assuming. A proposal settled\n\t\t// before the stamps keeps the block form, which is all it ever had.\n\t\tif p.readyTime != 0 {\n\t\t\tpanic(ufmt.Sprintf(\"govern: still in the delay window: executable at \"+\n\t\t\t\t\"%d (block %d), %d seconds away\",\n\t\t\t\tp.readyTime, p.ready, p.readyTime-g.voters.Now()))\n\t\t}\n\t\tnow := g.voters.Height()\n\t\tpanic(ufmt.Sprintf(\"govern: still in the delay window: executable at \"+\n\t\t\t\"height %d, %d blocks away\", p.ready, p.ready-now))\n\t}\n\te := g.entryOf(p.kind)\n\tif e == nil || !e.live {\n\t\t// The holders withdrew this power while the proposal was waiting.\n\t\t// Recorded as a failure rather than a panic: the proposal is finished\n\t\t// either way, and a panic would leave it Succeeded forever, retried by\n\t\t// anyone, failing identically each time.\n\t\tg.setState(p, stateFailed, \"the kind was retired before this could run\")\n\t\treturn\n\t}\n\tk := e.kind\n\n\t// Checked again, against the world as it is now rather than as it was when\n\t// the question was asked.\n\tif err := g.checkKind(k, p.payload); err != nil {\n\t\tg.setState(p, stateFailed, \"no longer valid: \"+err.Error())\n\t\treturn\n\t}\n\n\t// Deferred, not cleared on the next line. If Do panics the transaction\n\t// aborts and the flag rolls back with everything else — but a recover\n\t// anywhere between here and there would leave the latch set and the\n\t// governor permanently unable to run anything again.\n\tg.executing = true\n\tdefer func() { g.executing = false }()\n\t// The kind gets a SUB-REALM, never the realm's own capability — which is\n\t// why the sub-path is computed here and handed out, rather than left to\n\t// whoever writes the Dispatch.\n\t//\n\t// A live cur inside a foreign kind's Do has IsCurrent() true and the\n\t// consuming realm's PkgPath, so it is not \"the realm as data\", it is that\n\t// realm's authority, handed to code the holders adopted for one purpose.\n\t// With it a kind can cross() into any realm that trusts the consumer and be\n\t// seen as it, issue its tokens, and drain its banker. Adopting a treasury\n\t// kind would grant reach over every realm that names this one.\n\t//\n\t// cur.Sub gives a distinct pkgpath and a distinct address, and the VM\n\t// refuses RealmIssue for a sub-realm outright. A governed realm therefore\n\t// gates on the sub-path of the power it granted, which is also more useful\n\t// than gating on the governor as a whole.\n\tvar err error\n\tif b, ok := asBuiltin(k); ok {\n\t\t// The governor's own kinds act on the governor. They are handed it\n\t\t// directly and never a sub-realm: there is no outside world for them\n\t\t// to reach, so there is nothing to grant.\n\t\terr = b.run(g, run, p.payload)\n\t} else {\n\t\terr = run(k, subPathOf(p.kind), p.payload)\n\t}\n\n\tif err != nil {\n\t\tg.setState(p, stateFailed, err.Error())\n\t\treturn\n\t}\n\tg.setState(p, stateExecuted, \"\")\n}\n\n// ReleaseRoll drops a finished proposal's voter roll and refunds its deposit to\n// whoever calls.\n//\n// The roll does not go when a proposal settles, because a kind that pays out to\n// the people who voted has to ask after the fact, and pull-based claiming means\n// the last claim can be a long way after execution. So it is reclaimed on\n// request instead: nobody is obliged to, and whoever does is paid for it — the\n// same bargain Settle makes for the slot.\n//\n// Refused twice over. While a proposal is open, because Vote reads the roll to\n// refuse a second vote and dropping it early would let everybody vote twice.\n// And while it has SUCCEEDED but not yet run, because that is exactly the\n// window in which the kind has not read it yet — Execute is where a kind that\n// pays its voters looks, so a stranger could otherwise empty the roll in the\n// block before execution and leave the kind with nobody to pay. Waiting for the\n// execution to land costs the reclaimer nothing; there is no deadline on this.\nfunc (g *Governor) ReleaseRoll(who address, id int64) {\n\tp := g.mustProposal(id)\n\tg.settle(p)\n\tswitch p.state {\n\tcase stateActive:\n\t\tpanic(\"govern: that proposal is still open; its roll is what refuses a second vote\")\n\tcase stateSucceeded:\n\t\tpanic(\"govern: that proposal has not run yet; the roll is what it runs against\")\n\t}\n\tp.voted = nil\n}\n\n// Settle advances a proposal's state and frees its slot if it is finished.\n//\n// Needed because a read cannot persist: State and Render will tell you a\n// proposal is defeated and tell you again tomorrow, since the transition they\n// computed died with the query. Without a way to record it, the open list is a\n// resource anybody can exhaust — fill every slot with proposals that will lose\n// and nothing ever reclaims them.\n//\n// Permissionless, because it decides nothing: it writes down a conclusion the\n// rules already reached.\nfunc (g *Governor) Settle(who address, id int64) {\n\tg.settle(g.mustProposal(id))\n}\n\n// sweep frees the slots of a few finished proposals. Bounded, and run when\n// somebody proposes — the moment a full list matters, paid for by whoever wants\n// the room. Unbounded here would make one unlucky proposer tidy the history.\n//\n\nconst sweepScan = 8\n\nfunc (g *Governor) sweep() {\n\t// openIdx cannot exceed maxLive — Propose refuses at the bound — so the\n\t// whole ring is a handful of windows. Trying them until one frees a slot is\n\t// still a BOUNDED scan; the bound that matters here is the index's, not the\n\t// history's, and it is the history that the per-window limit exists to keep\n\t// one unlucky proposer from paying to tidy.\n\tfor w := 0; w \u003c= maxLive/sweepScan; w++ {\n\t\tbefore := g.openIdx.Size()\n\t\tg.sweepWindow()\n\t\tif g.openIdx.Size() \u003c before {\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (g *Governor) sweepWindow() {\n\tconst scan = sweepScan\n\n\t// Collected first, acted on after: settle removes from openIdx, and bptree\n\t// says a tree must not be modified during iteration (tree.gno). Calling it\n\t// from the callback removed the key the cursor stood on.\n\t//\n\t// No failure was observed and that is not evidence of safety — at this size\n\t// the tree tolerates it. The symptom, if it does not, is a slot that never\n\t// comes back, months later, under load.\n\tseen := 0\n\tlast := \"\"\n\tvar ids []string\n\tvar stale []string\n\tg.openIdx.Iterate(g.sweepFrom, \"\", func(k string, v any) bool {\n\t\tseen++\n\t\tlast = k\n\t\tif g.proposals.Get(v.(string)) == nil {\n\t\t\tstale = append(stale, k)\n\t\t} else {\n\t\t\tids = append(ids, v.(string))\n\t\t}\n\t\treturn seen \u003e= scan\n\t})\n\t// Where to resume. A short window means the end of the index was reached,\n\t// so the next one starts over; otherwise it starts just past the last key\n\t// looked at. Appending a NUL gives the smallest string above a key, and the\n\t// keys are all the same length, so nothing can sort between the two.\n\tif seen \u003c scan {\n\t\tg.sweepFrom = \"\"\n\t} else {\n\t\tg.sweepFrom = last + \"\\x00\"\n\t}\n\n\tfor _, key := range ids {\n\t\tif p := g.proposals.Get(key); p != nil {\n\t\t\tg.settle(p.(*proposal))\n\t\t}\n\t}\n\t// Index entries whose proposal has gone entirely, which settle cannot\n\t// reach because there is nothing left to settle.\n\tfor _, k := range stale {\n\t\tg.openIdx.Remove(k)\n\t}\n}\n\n// Cancel withdraws a proposal. Only the proposer, only while it is still open.\nfunc (g *Governor) Cancel(who address, id int64) {\n\tp := g.mustProposal(id)\n\tif who != p.proposer {\n\t\tpanic(\"govern: only the proposer may cancel\")\n\t}\n\tg.settle(p)\n\tif p.state != stateActive {\n\t\tpanic(\"govern: too late to cancel\")\n\t}\n\t// The one transition out of active that the rules do not make.\n\tg.setState(p, stateCanceled, \"\")\n}\n\n// ----------------------------------------------------------------- reading --\n\n// State advances the clock before answering, so a reader never sees a proposal\n// that is active only because nobody has poked it.\nfunc (g *Governor) State(id int64) string {\n\tp := g.mustProposal(id)\n\t// The pure form: asking what a proposal's state is must not change it.\n\tst, _ := g.wouldBe(p, g.voters.Height())\n\tswitch st {\n\tcase stateActive:\n\t\treturn \"active\"\n\tcase stateDefeated:\n\t\treturn \"defeated\"\n\tcase stateSucceeded:\n\t\treturn \"succeeded\"\n\tcase stateExecuted:\n\t\treturn \"executed\"\n\tcase stateFailed:\n\t\treturn \"failed\"\n\tcase stateCanceled:\n\t\treturn \"canceled\"\n\tdefault:\n\t\treturn \"expired\"\n\t}\n}\n\n// Preview renders what a payload would say, before anybody proposes it — so an\n// interface can show somebody their proposal, and Check refuses a malformed one\n// without spending a transaction.\n//\n// It also pins the thing the extension point rests on: govern calling INTO a\n// kind in another realm. A stored interface value is re-resolved from the store\n// on every call, so the method that runs is the offering realm's code.\n//\n// Reads whether or not the kind is adopted. What a payload would say is not a\n// power, and refusing to preview an unadopted one makes the adoption vote\n// harder to judge rather than safer.\nfunc (g *Governor) Preview(kind, payload string) string {\n\t// The same bound Propose applies, at the cheaper door. Propose is a\n\t// transaction and this is a read, so an unbounded payload here is the\n\t// easier of the two to hand over — and it reaches the same Describe and\n\t// Check with it. Bounding one door and not the other is not bounding\n\t// anything.\n\tif len(payload) \u003e maxPayload {\n\t\treturn \"that payload is too long\"\n\t}\n\te := g.entryOf(kind)\n\tif e == nil {\n\t\treturn \"no kind by that name has been offered\"\n\t}\n\tout := g.describeKind(e.kind, payload)\n\tif err := g.checkKind(e.kind, payload); err != nil {\n\t\tout += \"\\n\\n_This would be refused: \" + err.Error() + \"_\"\n\t}\n\tif !e.live {\n\t\tout += \"\\n\\n_This kind has not been adopted, so it cannot be proposed yet._\"\n\t}\n\treturn out\n}\n\n// Describe is what the vote is about, rendered by the kind from the payload\n// alone — the same string Do will be handed.\nfunc (g *Governor) Describe(id int64) string {\n\tp := g.mustProposal(id)\n\treturn g.describeOf(p)\n}\n\n// HasVoted reports whether an address has already voted on a proposal.\n//\n// OpenZeppelin has hasVoted; without it the only way to find out is to send a\n// vote and be refused, spending a transaction to learn what the realm knows. A\n// wallet needs it before deciding whether to offer the buttons.\n//\n// Keeps answering after a proposal closes, because the roll survives until\n// somebody calls ReleaseRoll. Once it has been released this answers false, so\n// it is \"is their vote still on record here\", not \"did they ever\" — the\n// permanent record of who voted is the Voted events.\nfunc (g *Governor) HasVoted(id int64, who address) bool {\n\tp := g.mustProposal(id)\n\treturn p.voted != nil \u0026\u0026 who != rollSentinel \u0026\u0026 p.voted.Has(string(who))\n}\n\n// secsPerBlock converts a VotingBlocks rule — configured in blocks, as every\n// governor rule is — into the wall-clock window it was chosen to mean.\nconst secsPerBlock = int64(5)\n\n// votingClosed reports whether a proposal has stopped taking votes.\n//\n// THE DEADLINE IS THE PROMISE, THE HEIGHT IS THE FALLBACK. Counted in blocks a\n// window advertised as \"four days\" is four days only while the chain holds its\n// assumed pace; the report that started this work showed a vote \"closing in ~7\n// days\" beside an answer dated five years earlier. A proposal opened before the\n// stamps existed has closesTime 0 and keeps the height it was opened under.\nfunc (g *Governor) votingClosed(p *proposal, now int64) bool {\n\tif p.closesTime != 0 {\n\t\treturn g.voters.Now() \u003e= p.closesTime\n\t}\n\treturn now \u003e= p.closes\n}\n\n// TimingsAt is Timings' wall-clock half: when the proposal opened and when\n// voting closes, as unix seconds, or 0 for a proposal opened before the stamps.\n//\n// A SIBLING RATHER THAN A WIDER Timings, because Timings is consumed by more\n// than one realm and arity is the kind of change that should arrive with its\n// readers rather than ahead of them. Its first reader lands in the same commit\n// as this method — kourtv2's ClaimTimeline — which is the rule the clock plan\n// applies to constants and which holds just as well for a read.\n//\n// The heights stay on Timings and are still the reference; this is the number\n// the gate actually compares against, so a page built from it cannot promise a\n// close the governor will not honour.\nfunc (g *Governor) TimingsAt(id int64) (openedTime, closesTime int64) {\n\tp := g.mustProposal(id)\n\treturn p.openedTime, p.closesTime\n}\n\n// Timings is a proposal's clock: when it opened, when voting closes, when it\n// becomes executable, and when that chance lapses. The companion to Tally —\n// without it, anything showing a countdown parsed the page, which is prose\n// written for people.\n//\n// Two of these are zero rather than absent, and both zeroes mean something:\n//\n//\tready == 0    the outcome is not recorded yet, so the timelock has not\n//\t              started. Settle starts it, and anybody may call it.\n//\texpires == 0  it never expires, which is what a grace period of zero means\n//\t              everywhere else in this realm.\nfunc (g *Governor) Timings(id int64) (opened, closes, ready, expires int64) {\n\tp := g.mustProposal(id)\n\tat, ever := expiresAt(p)\n\tif !ever {\n\t\tat = 0\n\t}\n\treturn p.opened, p.closes, p.ready, at\n}\n\n// Tally is the vote so far: yes, no, abstain, and the supply they are weighed\n// against.\n//\n// The fourth number is the one worth having. Every bar here is a fraction —\n// quorum of the snapshot supply, threshold of what was cast — so three counts\n// without their denominator cannot be checked against anything. It is the\n// supply at the proposal's snapshot epoch, not the supply now.\nfunc (g *Governor) Tally(id int64) (yes, no, abstain, total int64) {\n\tp := g.mustProposal(id)\n\treturn p.yes, p.no, p.abstain, p.total\n}\n\n// Render is the governance page.\n//\n// It matters more here than a Render usually does. This design's whole claim\n// over one that carries code is that what a voter reads IS what executes —\n// Describe and Do consume the same string. That claim is only worth anything\n// if somebody actually reads it, so the description is on the page rather than\n// behind a call nobody makes.\nfunc (g *Governor) render(path, notes string) string {\n\tif path != \"\" {\n\t\treturn g.renderOne(path)\n\t}\n\tout := ufmt.Sprintf(\"# %s (%s)\\n\\n\", g.token.Name(), g.token.Symbol())\n\n\t// The token first, because the governance below is only meaningful in\n\t// terms of it: every quorum is a fraction of this supply and every vote is\n\t// weighed out of it.\n\tout += ufmt.Sprintf(\"- supply: %s %s (%d at %d decimals)\\n\",\n\t\tg.units(g.token.TotalSupply()), g.token.Symbol(), g.token.TotalSupply(), g.token.Decimals())\n\tout += notes\n\tout += ufmt.Sprintf(\"- epoch: %d (votes are weighed at sealed epochs)\\n\\n\", g.voters.Epoch())\n\tout += \"## governance\\n\\n\"\n\n\tvar open, ready, done string\n\tnOpen := 0\n\tnow := g.voters.Height()\n\n\t// The open list IS openIdx: a proposal holds its slot until it reaches a\n\t// final state, so what is in there is exactly what belongs above the fold.\n\t// Bounded by maxLive rather than by how many questions have ever been\n\t// asked.\n\tg.openIdx.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tpv := g.proposals.Get(v.(string))\n\t\tif pv == nil {\n\t\t\treturn false\n\t\t}\n\t\tp := pv.(*proposal)\n\t\tst, _ := g.wouldBe(p, now)\n\t\tif st != stateActive \u0026\u0026 st != stateSucceeded {\n\t\t\t// Finished, and still holding its slot because nobody has written\n\t\t\t// that down. The scan below reaches it as history, which is what\n\t\t\t// it is.\n\t\t\treturn false\n\t\t}\n\t\tif st == stateSucceeded {\n\t\t\tready += proposalLine(p, st)\n\t\t} else {\n\t\t\topen += proposalLine(p, st)\n\t\t}\n\t\tnOpen++\n\t\treturn false\n\t})\n\n\t// Newest first, and it stops as soon as the tail is full.\n\t//\n\t// The cap is on the WORK, not just on what gets appended. One pass over\n\t// every proposal the realm has ever held, computing each state and\n\t// truncating at the end, would grow forever while the page stayed the same\n\t// size — and this is the read every holder makes.\n\tg.proposals.ReverseIterate(\"\", \"\", func(_ string, v any) bool {\n\t\tif len(done) \u003e= 2000 {\n\t\t\treturn true\n\t\t}\n\t\tp := v.(*proposal)\n\t\tst, _ := g.wouldBe(p, now)\n\t\tif st == stateActive || st == stateSucceeded {\n\t\t\treturn false // already above, from g.openIdx\n\t\t}\n\t\tdone += proposalLine(p, st)\n\t\treturn false\n\t})\n\n\t// Split by what the reader is asked to DO, not by which list the realm\n\t// keeps them in. Both hold a slot, but a decided proposal waiting out its\n\t// timelock is not something to vote on, and putting it under a heading that\n\t// means \"still being voted on\" tells the reader to do the wrong thing.\n\t//\n\t// The slot count belongs to the page rather than to either section. nOpen\n\t// counts everything holding a slot, decided-and-unrun included, so under a\n\t// voting heading it would sit above a shorter list — and vanish entirely\n\t// when every slot is held by decided proposals, which is when being full\n\t// matters most.\n\t// And the lane, once it is the only thing left. Below that point the count\n\t// is the whole story; at or above it, a holder reads free slots and is\n\t// refused anyway, which is the page contradicting the realm.\n\tout += ufmt.Sprintf(\"%d of %d slots in use\", nOpen, maxLive)\n\tif nOpen \u003e= maxOpen {\n\t\tout += ufmt.Sprintf(\"; the last %d are kept for the governor's own kinds, \"+\n\t\t\t\"so nothing else can be opened until one comes back\", maxLive-maxOpen)\n\t}\n\tout += \".\\n\\n\"\n\tif open == \"\" {\n\t\tout += \"_Nothing to vote on._\\n\"\n\t} else {\n\t\tout += \"### to vote on\\n\\n\" + open\n\t}\n\tif ready != \"\" {\n\t\tout += \"\\n### decided, waiting to be run\\n\\n\" + ready\n\t}\n\tif done != \"\" {\n\t\tout += \"\\n### finished\\n\\n\" + done\n\t}\n\treturn out\n}\n\nfunc (g *Governor) renderOne(path string) string {\n\tid, err := strconv.ParseInt(path, 10, 64)\n\tif err != nil {\n\t\treturn \"# not a proposal id\\n\"\n\t}\n\tv := g.proposals.Get(enc(uint64(id)))\n\tif v == nil {\n\t\treturn \"# no such proposal\\n\"\n\t}\n\tp := v.(*proposal)\n\tst, reason := g.wouldBe(p, g.voters.Height())\n\n\tout := ufmt.Sprintf(\"# #%d — %s\\n\\n\", p.id, p.title)\n\t// The description first, and rendered by the kind from the payload alone.\n\t// This is the text the vote is about.\n\tout += g.describeOf(p) + \"\\n\\n\"\n\tout += ufmt.Sprintf(\"- kind: `%s`\\n- proposer: %s\\n- state: **%s**\\n\",\n\t\tp.kind, p.proposer.String(), stateName(st))\n\tif reason != \"\" {\n\t\tout += \"- reason: \" + reason + \"\\n\"\n\t}\n\t// The snapshot supply, rendered as the header renders the live one — it is\n\t// the same quantity, and it was raw here and in units there.\n\t//\n\t// The tally BELOW stays raw on purpose. Its stated job is to let a reader\n\t// check the arithmetic rather than take the state on trust, and mixing\n\t// units into numbers somebody is about to multiply would take that away.\n\t// Two renderings of the same kind of figure, for two different jobs, on one\n\t// page — which is worth saying because it looks like the inconsistency it\n\t// was just fixed for.\n\tout += ufmt.Sprintf(\"- weighed at epoch %d, when %s %s was in issue (%d)\\n\",\n\t\tp.epoch, g.units(p.total), g.token.Symbol(), p.total)\n\tout += ufmt.Sprintf(\"\\n## votes\\n\\n- yes %d\\n- no %d\\n- abstain %d\\n\",\n\t\tp.yes, p.no, p.abstain)\n\t// Both bars, as counted, so a reader can check the arithmetic rather than\n\t// take the state on trust.\n\tcast := turnout(p)\n\t// Percentages, not basis points. The realm counts in bps because integers\n\t// are exact where a float is an argument waiting to happen, and pct() exists\n\t// precisely so that choice does not reach the page — a holder asked to read\n\t// \"needs 6600 bps\" is being asked to do arithmetic to find out whether their\n\t// vote mattered.\n\t// Against ENGAGED, because that is what quorum divides by. Equal to total\n\t// for this realm; an electorate that drops idle weight would otherwise show\n\t// a bar nobody is being measured against.\n\t//\n\t// A consumer-set absolute floor is rendered as the count it is, not a\n\t// fraction of engaged — the tally compares cast against that number, so the\n\t// page must show it or it contradicts the state it is describing.\n\tif p.quorumFloor \u003e 0 {\n\t\tout += ufmt.Sprintf(\"\\nturnout %d of %d, needs %d · \", cast, p.engaged, p.quorumFloor)\n\t} else {\n\t\tout += ufmt.Sprintf(\"\\nturnout %d of %d, needs %s · \", cast, p.engaged, pct(p.rules.QuorumBps))\n\t}\n\t// Three cases, not two. The threshold weighs yes against yes+no, so it has\n\t// nothing to weigh when every vote was an abstention — but that is not the\n\t// same as nobody voting, and collapsing them prints a line that contradicts\n\t// itself:\n\t//\n\t//\tturnout 1000 of 1000, needs 20.00% · no votes cast\n\t//\n\t// The whole supply voted. Saying so on the left and denying it on the right\n\t// is worst for the one case this realm treats most carefully: an\n\t// all-abstained proposal is the one that would otherwise pass with nobody\n\t// in favour, which is why the support bar exists at all.\n\tswitch {\n\tcase forAndAgainst(p) \u003e 0:\n\t\tout += ufmt.Sprintf(\"yes is %d of %d cast, needs %s\\n\",\n\t\t\tp.yes, forAndAgainst(p), pct(p.rules.ThresholdBps))\n\tcase p.abstain \u003e 0:\n\t\tout += \"every vote was an abstention, so the threshold has nothing to weigh\\n\"\n\tdefault:\n\t\tout += \"no votes cast\\n\"\n\t}\n\n\t// When voting closes. The block below was written for the succeeded state\n\t// on the reasoning that it is the one state where a reader has something to\n\t// do and no way to know when — which was right, and stopped one state\n\t// short: an active proposal is where they have the most to do.\n\t//\n\t// The subtraction is safe. wouldBe returns a terminal state once now has\n\t// reached p.closes, so a proposal reading active has not.\n\tif st == stateActive {\n\t\t// THE DATE, not a block count. \"closes in 120,960 blocks\" is a promise\n\t\t// only a reader who knows the chain's pace can check, and the pace is not\n\t\t// a promise — that mismatch is what put a vote \"closing in ~7 days\" beside\n\t\t// an answer dated five years earlier. A proposal opened before the stamps\n\t\t// has no date and keeps the block form it was opened under.\n\t\tif p.closesTime != 0 {\n\t\t\t// A COUNTDOWN AND AN ABSOLUTE, because they answer different\n\t\t\t// questions: the count tells a reader how long they have, and the\n\t\t\t// date is what the gate will actually compare against, so a page\n\t\t\t// read at one moment is still checkable at the next. The old line\n\t\t\t// gave a count in BLOCKS, which is only a duration to a reader who\n\t\t\t// knows the chain's pace — and the pace is what was wrong.\n\t\t\tout += ufmt.Sprintf(\"\\n_Voting closes in %d seconds, at %d (block %d)._\\n\",\n\t\t\t\tp.closesTime-g.voters.Now(), p.closesTime, p.closes)\n\t\t} else {\n\t\t\tout += ufmt.Sprintf(\"\\n_Voting closes in %d blocks, at height %d._\\n\",\n\t\t\t\tp.closes-g.voters.Height(), p.closes)\n\t\t}\n\t}\n\n\t// What happens next, for a decision that has been taken and not yet done.\n\t//\n\t// Without this the page says \"succeeded\" and stops, which is the one state\n\t// where a reader has something to DO and no way to know when. It is also\n\t// the state that looks most like nothing is happening: the proposal sits on\n\t// the open list, apparently unfinished, while a timelock it cannot see runs\n\t// down.\n\tif st == stateSucceeded {\n\t\tnow := g.voters.Height()\n\t\texpiry, expires := expiresAt(p)\n\t\tswitch {\n\t\tcase p.state != stateSucceeded:\n\t\t\t// Decided and not yet written down — what a proposal winning on its\n\t\t\t// DEADLINE looks like, since nobody has to be present when one\n\t\t\t// passes.\n\t\t\t//\n\t\t\t// p.ready is zero until then, so the lines below would be\n\t\t\t// arithmetic against nothing — \"succeeded\" and \"expired\" printed\n\t\t\t// together. Saying what is actually needed is the useful thing\n\t\t\t// anyway, because anybody may Settle it.\n\t\t\tout += \"\\n_Decided, and nobody has recorded it yet. The delay \" +\n\t\t\t\t\"starts when somebody calls Settle — anybody may._\\n\"\n\t\tcase g.inDelay(p):\n\t\t\t// SECONDS AND THE ABSOLUTE, for the reason the voting close carries\n\t\t\t// both: a count in blocks is a duration only to a reader who knows\n\t\t\t// the chain's pace. A proposal settled before the stamps keeps the\n\t\t\t// block form, which is all it ever had.\n\t\t\tif p.readyTime != 0 {\n\t\t\t\tout += ufmt.Sprintf(\"\\n_Waiting: executable in %d seconds, at %d (block %d)._\\n\",\n\t\t\t\t\tp.readyTime-g.voters.Now(), p.readyTime, p.ready)\n\t\t\t} else {\n\t\t\t\tout += ufmt.Sprintf(\"\\n_Waiting: executable in %d blocks, at height %d._\\n\",\n\t\t\t\t\tp.ready-now, p.ready)\n\t\t\t}\n\t\tcase g.expired(p):\n\t\t\tout += \"\\n_Expired: nobody executed it in time._\\n\"\n\t\tcase expires:\n\t\t\tout += ufmt.Sprintf(\"\\n**Executable now**, until height %d.\\n\", expiry)\n\t\tdefault:\n\t\t\tout += \"\\n**Executable now.**\\n\"\n\t\t}\n\t}\n\treturn out\n}\n\n// proposerBar renders the stake a proposer must hold, where zero is not \"none\n// required\" so much as \"no check at all\".\nfunc proposerBar(bps int64) string {\n\tif bps \u003c= 0 {\n\t\treturn \"anybody, holding nothing\"\n\t}\n\treturn pct(bps) + \" of the supply\"\n}\n\n// turnout is every vote cast, abstentions included. One definition because it\n// was two: the tally computed it and the page computed it again, so the quorum\n// a proposal was DECIDED by and the figure a holder READ could diverge. That\n// line has already produced two page-versus-state contradictions.\n//\n// Abstain counts here because it counts towards quorum, which is why the\n// support bar exists separately.\nfunc turnout(p *proposal) int64 { return p.yes + p.no + p.abstain }\n\n// forAndAgainst is what the threshold weighs: the votes that took a side.\n//\n// Abstentions are deliberately absent — they count towards quorum and not\n// towards the bar — and that rule was written once in the tally and again on\n// the page, which is how a page comes to disagree with the state it describes.\nfunc forAndAgainst(p *proposal) int64 { return p.yes + p.no }\n\n// inDelay is whether a decided proposal is still inside its timelock.\n//\n// Execute refuses while this holds and the page counts down while it holds, and\n// those two must not disagree by even a block: a page offering to run what\n// Execute will refuse is the same defect as a page calling a live proposal\n// expired. One definition, so a boundary cannot be moved on one side only.\nfunc (g *Governor) inDelay(p *proposal) bool {\n\tif p.readyTime != 0 {\n\t\treturn g.voters.Now() \u003c p.readyTime\n\t}\n\treturn g.voters.Height() \u003c p.ready\n}\n\n// expired reports whether a decided proposal is past its execution window.\n//\n// THE TWO WINDOWS MOVE TOGETHER, and that is why they convert together: the\n// delay exists so people can react, and the grace exists so a decision about a\n// world that has moved cannot be executed. Both are measured from p.ready, so\n// converting one and not the other would let a proposal leave its delay on one\n// clock and expire on the other — a gap where it is neither waiting nor\n// executable, or an overlap where it is both.\nfunc (g *Governor) expired(p *proposal) bool {\n\tif at, ever := expiresAtTime(p); ever {\n\t\treturn g.voters.Now() \u003e at\n\t}\n\tat, ever := expiresAt(p)\n\treturn ever \u0026\u0026 g.voters.Height() \u003e at\n}\n\n// expiresAtTime is expiresAt in wall-clock seconds, or (0, false) for a\n// proposal settled before the stamps existed.\nfunc expiresAtTime(p *proposal) (int64, bool) {\n\tif p.rules.GraceBlocks \u003c= 0 || p.readyTime == 0 {\n\t\treturn 0, false\n\t}\n\treturn p.readyTime + p.rules.GraceBlocks*secsPerBlock, true\n}\n\n// expiresAt is when a decided proposal stops being executable, and whether it\n// ever does. saneRules refuses a grace of zero, so in practice it always does.\n//\n// One definition because it is easy to write three: the tally testing\n// `now \u003e ready+grace`, the page testing it again, and the page printing the sum\n// a third time as the height it offers to run until. Three independent copies\n// of one deadline is how a page comes to print \"succeeded\" and \"expired\"\n// together.\nfunc expiresAt(p *proposal) (int64, bool) {\n\t// Nothing expires before the outcome is written down, because the window is\n\t// measured from p.ready and p.ready is zero until Settle sets it. Adding a\n\t// grace period to nothing gives a deadline in the realm's first hour, which\n\t// is how a page once printed \"succeeded\" and \"expired\" together.\n\t//\n\t// The page guards that by checking the recorded state before it asks; the\n\t// guard belongs here, where the arithmetic is. Timings walked straight into\n\t// it the moment it exposed this to a caller who had not read the page's\n\t// comment.\n\t// saneRules refuses a grace of zero, so that arm is defence rather than\n\t// policy; p.ready is the live one.\n\tif p.rules.GraceBlocks \u003c= 0 || p.ready == 0 {\n\t\treturn 0, false\n\t}\n\treturn p.ready + p.rules.GraceBlocks, true\n}\n\n// span renders a period in blocks, with roughly how long that is.\n//\n// Correctness never depends on the wall clock, but \"120960 blocks to vote\"\n// does not tell a reader whether that is an afternoon or a season. Both are\n// printed and the approximation is marked as one.\n//\n// Five seconds a block, the same assumption the bootstrap rules were chosen\n// under. At another cadence the block counts are still exact and the hours are\n// not, which is why they are hedged.\nfunc span(blocks int64) string {\n\tif blocks \u003c= 0 {\n\t\treturn \"no period\"\n\t}\n\tsecs := blocks * 5\n\tswitch {\n\tcase secs \u003c 60*60:\n\t\treturn ufmt.Sprintf(\"%d blocks (about %s)\", blocks, plural(secs/60, \"minute\"))\n\tcase secs \u003c 48*60*60:\n\t\treturn ufmt.Sprintf(\"%d blocks (about %s)\", blocks, plural(secs/3600, \"hour\"))\n\tdefault:\n\t\treturn ufmt.Sprintf(\"%d blocks (about %s)\", blocks, plural(secs/86400, \"day\"))\n\t}\n}\n\n// plural writes a count with its unit, in the number the count deserves.\n//\n// \"1 hours\" reads as something a machine wrote, and a page that reads as\n// machine-written gets skimmed — which is a poor outcome for the text somebody\n// is meant to study before granting a power.\nfunc plural(n int64, unit string) string {\n\tif n == 1 {\n\t\treturn \"1 \" + unit\n\t}\n\treturn ufmt.Sprintf(\"%d %ss\", n, unit)\n}\n\n// units renders a base-unit amount the way a holder counts it. The ledger is\n// integers all the way down, which is exact and unreadable: \"supply: 1000\"\n// beside \"decimals: 6\" asks somebody to divide before they know whether they\n// hold a millionth of the token or all of it. Both are printed — this for\n// reading, the raw figure for checking.\n//\n// scale is 10^decimals for the token being rendered — computed, not a constant,\n// because Go has no constant exponentiation and the engine does not know what token\n// it renders until given one (TestTheEngineRendersTheTokensOwnScale checks it).\n// Decimals is CLAMPED to 18 first: 10^19 overflows int64 (and 10^k for k≥64 wraps to\n// 0, which would divide-by-zero in units()), so an unclamped power on a pathological\n// token would corrupt or panic Render. Render is a read, so a clamp on the rare bad\n// token is the right trade; grc20votes reports a small fixed decimals and is unaffected.\nfunc (g *Governor) scale() int64 {\n\td := g.token.Decimals()\n\tif d \u003e 18 {\n\t\td = 18\n\t}\n\tout := int64(1)\n\tfor i := 0; i \u003c d; i++ {\n\t\tout *= 10\n\t}\n\treturn out\n}\n\nfunc (g *Governor) units(n int64) string {\n\twhole := n / g.scale()\n\tfrac := n % g.scale()\n\t// The sign has to be taken from n, not from the whole part. Integer\n\t// division truncates towards zero, so everything between -1 and 0 has a\n\t// whole part of exactly 0 — and a \"-0\" that FormatInt prints as \"0\". Taking\n\t// the minus from whole therefore dropped it for that entire range, and\n\t// -0.5 rendered as 0.5: not a near miss but the opposite number.\n\tsign := \"\"\n\tif n \u003c 0 {\n\t\tsign = \"-\"\n\t\twhole, frac = -whole, -frac\n\t}\n\tout := strconv.FormatInt(frac, 10)\n\tfor len(out) \u003c g.token.Decimals() {\n\t\tout = \"0\" + out\n\t}\n\t// Trailing zeroes are noise; a whole number should read as one.\n\tfor len(out) \u003e 1 \u0026\u0026 out[len(out)-1] == '0' {\n\t\tout = out[:len(out)-1]\n\t}\n\tif out == \"0\" {\n\t\treturn sign + strconv.FormatInt(whole, 10)\n\t}\n\treturn sign + strconv.FormatInt(whole, 10) + \".\" + out\n}\n\n// proposalLine is one row of the governance list.\nfunc proposalLine(p *proposal, st int8) string {\n\treturn ufmt.Sprintf(\"- [#%d](:%d) **%s** — `%s` · %s\\n\",\n\t\tp.id, p.id, p.title, p.kind, stateName(st))\n}\n\n// maxReason bounds what a kind may write into the permanent record.\n//\n// An adopted kind is trusted — the holders voted it the power to run — so this\n// is not a defence against a hostile one, which could do far worse than store a\n// long string. It is a defence against a buggy one: an error carrying a whole\n// response body or a stack of context lands in a proposal that is kept forever\n// and rendered every time somebody opens it.\nconst maxReason = 256\n\n// clip truncates at a rune boundary, so a cut message stays valid UTF-8 rather\n// than ending in half a character.\nfunc clip(s string) string {\n\tif len(s) \u003c= maxReason {\n\t\treturn s\n\t}\n\t// Walk back to the first byte that is not a continuation byte. Cutting\n\t// BEFORE that byte is already a whole-rune prefix — the character it\n\t// begins is simply left out.\n\t//\n\t// Backing off one further, to drop the lead byte as well, is wrong and\n\t// looked right: it takes the cut inside the character before, which is how\n\t// this was first written. The test that caught it decodes what survives\n\t// rather than measuring it.\n\tn := maxReason\n\tfor n \u003e 0 \u0026\u0026 s[n]\u00260xC0 == 0x80 {\n\t\tn--\n\t}\n\treturn s[:n] + \"…\"\n}\n\n// proposalSettledEvent is the single announcement of an outcome.\n//\n// One event with a state on it rather than one event per outcome, because the\n// per-outcome shape is what let four of the six transitions go unannounced:\n// Executed and Failed were emitted at two of Execute's paths, its other\n// failure path and the retired-kind path said nothing, and a proposal that\n// simply LOST — the ordinary result — was never announced anywhere. It opened,\n// it collected votes, and then nothing was ever said about it again.\nconst (\n\t// kindOfferedEvent is the only announcement a kind gets before it is\n\t// adopted, and the only one at all if it never is.\n\t//\n\t// The registry is never iterated — an ungated one is unbounded, and walking\n\t// it would put somebody's spam on the page holders read — so without this\n\t// an offered kind could be found only by guessing its name, and the\n\t// extension point was undiscoverable.\n\t//\n\t// Adoption gets no event: it happens through a proposal, which is already\n\t// announced when it opens and when it settles.\n\tkindOfferedEvent = \"KindOffered\"\n\n\tproposalOpenedEvent  = \"ProposalOpened\"\n\tvotedEvent           = \"Voted\"\n\tproposalSettledEvent = \"ProposalSettled\"\n)\n\n// setState is the only writer of p.state, and does everything that follows from\n// a proposal reaching one, so no new code path has to remember. Called only\n// with a state out of stateActive: nothing transitions back in.\n//\n// Three things:\n//\n//   - the reason is clipped, so a kind cannot write an unbounded string into a\n//     record kept for the life of the realm;\n//   - the slot goes, unless the proposal SUCCEEDED and is pending execution;\n//   - the outcome is announced.\n//\n// Not the voter roll, which outlives the decision. A kind that pays the people\n// who voted has to ask after the proposal has finished, so the roll goes on\n// request instead, at ReleaseRoll — permissionless, with the deposit going to\n// whoever calls it, which is the same bargain Settle makes for the slot.\nfunc (g *Governor) setState(p *proposal, st int8, reason string) {\n\tp.state, p.reason = st, clip(reason)\n\tif st != stateSucceeded {\n\t\tg.release(p)\n\t}\n\tchain.Emit(proposalSettledEvent,\n\t\t\"id\", strconv.FormatInt(p.id, 10),\n\t\t\"state\", stateName(st),\n\t\t\"reason\", p.reason,\n\t)\n}\n\nfunc stateName(s int8) string {\n\tswitch s {\n\tcase stateActive:\n\t\treturn \"active\"\n\tcase stateDefeated:\n\t\treturn \"defeated\"\n\tcase stateSucceeded:\n\t\treturn \"succeeded\"\n\tcase stateExecuted:\n\t\treturn \"executed\"\n\tcase stateFailed:\n\t\treturn \"failed\"\n\tcase stateCanceled:\n\t\treturn \"canceled\"\n\t}\n\treturn \"expired\"\n}\n\nfunc (g *Governor) entryOf(name string) *entry {\n\tif v := g.kinds.Get(name); v != nil {\n\t\treturn v.(*entry)\n\t}\n\treturn nil\n}\n\n// subPathOf maps a kind name onto a legal sub-realm path.\n//\n// Sub takes '/'-separated segments of [a-z0-9] with '_.-' inside a segment, and\n// a kind name is any printable ASCII without spaces — \"govern:adopt\" among\n// them. Anything outside the alphabet becomes '-', which keeps the mapping\n// total, stable and readable: a governed realm gating on this sees the power it\n// granted rather than the governor as a whole.\nfunc subPathOf(kind string) string {\n\tout := []byte(kind)\n\tfor i := 0; i \u003c len(out); i++ {\n\t\tc := out[i]\n\t\tswitch {\n\t\tcase c \u003e= 'a' \u0026\u0026 c \u003c= 'z', c \u003e= '0' \u0026\u0026 c \u003c= '9':\n\t\tcase c \u003e= 'A' \u0026\u0026 c \u003c= 'Z':\n\t\t\tout[i] = c + ('a' - 'A')\n\t\tdefault:\n\t\t\tout[i] = '-'\n\t\t}\n\t}\n\treturn string(out)\n}\n\n// liveEntry is what a proposal needs: a kind the holders have adopted.\n//\n// The two failures are told apart because the remedy differs. A name nobody\n// offered is a typo; a name that IS offered and not adopted is waiting on a\n// vote, and \"no such kind\" points that reader at their spelling when what they\n// need is govern:adopt. Preview has always distinguished them.\nfunc (g *Governor) liveEntry(name string) *entry {\n\te := g.entryOf(name)\n\tif e == nil {\n\t\tpanic(\"govern: no such kind: \" + name)\n\t}\n\tif !e.live {\n\t\tpanic(\"govern: the kind \" + name + \" has been offered but not adopted, \" +\n\t\t\t\"so it cannot be proposed yet — the holders adopt it with govern:adopt\")\n\t}\n\treturn e\n}\n\nfunc (g *Governor) kindOf(name string) Kind { return g.liveEntry(name).kind }\n\n// anyKind is the kind whether or not it is still adopted.\n//\n// Reading is not doing. Retiring a kind withdraws the power to run it; it must\n// not also erase the record of what was already proposed under it — and it did,\n// because everything that renders a proposal went through the live lookup and\n// panicked. One retirement would have taken every historical page with it.\nfunc (g *Governor) anyKind(name string) Kind {\n\te := g.entryOf(name)\n\tif e == nil {\n\t\treturn nil\n\t}\n\treturn e.kind\n}\n\n// describeOf renders a proposal, surviving a kind that has since been retired\n// or was never offered here at all.\nfunc (g *Governor) describeOf(p *proposal) string {\n\tk := g.anyKind(p.kind)\n\tif k == nil {\n\t\treturn \"_the kind `\" + p.kind + \"` is no longer registered, so this \" +\n\t\t\t\"proposal can no longer describe itself. Its payload was:_\\n\\n\" +\n\t\t\tg.indented(p.payload)\n\t}\n\treturn g.describeKind(k, p.payload)\n}\n\n// indented puts EVERY line of a payload inside the code block. Four spaces in\n// front of the whole string indents line one and leaves the rest as live\n// markdown, and payloads may contain newlines — govern:batch is defined in\n// terms of them.\n//\n// Reached only when a kind has been retired or was never registered, which is\n// the one place a payload is rendered with nothing left to have vetted it: the\n// validation belonged to that kind's Check.\nfunc (g *Governor) indented(s string) string {\n\treturn \"    \" + strings.ReplaceAll(s, \"\\n\", \"\\n    \")\n}\n\nfunc (g *Governor) rulesOf(name string) Rules { return g.liveEntry(name).rules }\n\nfunc (g *Governor) mustProposal(id int64) *proposal {\n\tv := g.proposals.Get(enc(uint64(id)))\n\tif v == nil {\n\t\tpanic(\"govern: no such proposal\")\n\t}\n\treturn v.(*proposal)\n}\n\n// Errors a kind returns. Values rather than strings built at the call site, so\n// a caller can compare them and Describe cannot drift from Check.\n\ntype govError struct{ s string }\n\nfunc (e *govError) Error() string { return e.s }\nfunc govErr(s string) error       { return \u0026govError{s} }\n\n// digest identifies a question by what it asks, not by who asked it.\n//\n// Only ever used to refuse a duplicate while one is open. Identity stays a\n// sequence number, because a number renders, sorts and paginates and a hash\n// does none of those — OpenZeppelin derives the whole proposal id from the\n// hash and pays for it every time a UI wants a list in order.\nfunc (g *Governor) digest(kind, payload string) string {\n\th := sha256.Sum256([]byte(kind + \"\\x00\" + payload))\n\treturn string(h[:16])\n}\n\n// release frees a proposal's slot in the open index. Idempotent, because\n// settle can reach a terminal state from more than one path.\nfunc (g *Governor) release(p *proposal) { g.openIdx.Remove(g.digest(p.kind, p.payload)) }\n"},{"name":"introspect.gno","body":"package governor\n\n// What a consumer can ask that the pages already say.\n//\n// Render publishes most of this — the slot count, a proposal's epoch, why one\n// ended — and a caller that has to scrape a markdown page for a number the\n// engine holds is an engine with an incomplete API. These are the reads that\n// close that gap.\n//\n// None of them hands back a pointer. That is the rule the whole package is\n// built to (gno-security-guide.md §3(B)): a /p/-declared type can be named by\n// another /p/ package, so an exported *proposal or a live tree handle would let\n// a stranger declare a mutator over it and have the storage-realm borrow run it\n// under the CONSUMING realm's authority. Counts, copies and strings only.\n\n// The board's dimensions, which the front page prints and a caller may want to\n// reason about before proposing.\nconst (\n\t// MaxLive is how many proposals may stand at once.\n\tMaxLive = maxLive\n\n\t// MaxOpen is how many of those an ordinary kind may fill. The difference\n\t// is kept for the governor's own kinds, so a flood of one thing cannot\n\t// stop the holders changing the rules.\n\tMaxOpen = maxOpen\n\n\t// GovLanes is that difference.\n\tGovLanes = govLanes\n\n\t// The bounds on a proposal's three strings, which doc.gno publishes as a\n\t// table because they decide what a proposer can say before finding out by\n\t// transaction.\n\tMaxTitle    = maxTitle\n\tMaxPayload  = maxPayload\n\tMaxKindName = maxKindName\n\n\t// MaxBatch is how many members one govern:batch may carry.\n\tMaxBatch = maxBatch\n\n\t// Bps is the basis-point scale every bar here is expressed in.\n\tBps = bps\n\n\t// MaxReason is the longest failure message a kind can put on the record.\n\t// A kind returns the string and the governor keeps it for the life of the\n\t// realm, so it is clipped rather than trusted.\n\tMaxReason = maxReason\n\n\t// Reserved is the prefix the governor's own kinds carry, which Offer\n\t// refuses so that nothing published can render as a built-in.\n\tReserved = reserved\n)\n\n// OpenSlots is how many proposals are holding a slot, decided-and-unrun\n// included. The number the front page prints beside MaxLive.\nfunc (g *Governor) OpenSlots() int { return g.openIdx.Size() }\n\n// Proposals is how many questions have ever been asked here.\nfunc (g *Governor) Proposals() int { return g.proposals.Size() }\n\n// StoredState is the state that has been WRITTEN DOWN, which is not always the\n// state a reader is shown.\n//\n// State computes the outcome from the rules and the clock, because a proposal\n// whose deadline passed is decided whether or not anybody has poked it. Only a\n// transaction records that, so between the deadline and the next Settle the two\n// differ — and the difference is the whole reason reading is free: a read works\n// out the answer and stores nothing.\n//\n// A caller wanting to know whether the slot has actually been given back, or\n// whether there is anything left for Settle to do, wants this one.\nfunc (g *Governor) StoredState(id int64) string { return stateName(g.mustProposal(id).state) }\n\n// Reason is why a proposal ended, or the empty string while it is still open.\n// The page prints it; this is the same string.\nfunc (g *Governor) Reason(id int64) string { return g.mustProposal(id).reason }\n\n// EpochOf is the sealed epoch a proposal's weight was read at. Fixed when the\n// question was asked, and the reason buying in afterwards buys nothing.\nfunc (g *Governor) EpochOf(id int64) uint32 { return g.mustProposal(id).epoch }\n\n// Snapshot is a proposal's engaged weight and the sealed epoch both it and the\n// votes were read at — snapshotted at Propose and reachable no other way (EpochOf\n// gives the epoch alone). Engaged is the turnout DENOMINATOR the pool is shown\n// against; for a proposal opened with an absolute quorum floor the bar is that\n// floor, not a fraction of engaged — read it with QuorumFloor. Scalars, so nothing\n// is handed out to write through.\nfunc (g *Governor) Snapshot(id int64) (engaged int64, epoch uint32) {\n\tp := g.mustProposal(id)\n\treturn p.engaged, p.epoch\n}\n\n// QuorumFloor is the absolute turnout a proposal needs when it was opened with\n// ProposeWithQuorum, or 0 when the quorum is the rules' QuorumBps fraction of the\n// engaged weight. A court renders a floored proposal's bar from this; the engine\n// already uses it in the tally and on its own page. Snapshotted at Propose, so it\n// is the bar the voters actually face.\nfunc (g *Governor) QuorumFloor(id int64) int64 { return g.mustProposal(id).quorumFloor }\n\n// RollSize is how many addresses are on a proposal's voter roll.\n//\n// Zero can mean two things and HasRoll separates them: nobody has voted yet, or\n// the roll was reclaimed after the proposal finished.\nfunc (g *Governor) RollSize(id int64) int {\n\tp := g.mustProposal(id)\n\tif p.voted == nil {\n\t\treturn 0\n\t}\n\t// The sentinel Propose plants is not a voter. See rollSentinel.\n\tn := p.voted.Size()\n\tif p.voted.Has(rollSentinel) {\n\t\tn--\n\t}\n\treturn n\n}\n\n// HasRoll reports whether a proposal is still holding its voter roll, or has\n// given it back through ReleaseRoll.\nfunc (g *Governor) HasRoll(id int64) bool { return g.mustProposal(id).voted != nil }\n\n// KindStatus is whether a name has been offered here, and whether the holders\n// have adopted it. Both false for a name nobody has published.\nfunc (g *Governor) KindStatus(name string) (offered, live bool) {\n\te := g.entryOf(name)\n\tif e == nil {\n\t\treturn false, false\n\t}\n\treturn true, e.live\n}\n\n// KindRules is the terms a kind was adopted on. A copy: Rules is a plain struct\n// of numbers, so handing one back gives away nothing to write through.\nfunc (g *Governor) KindRules(name string) (Rules, bool) {\n\te := g.entryOf(name)\n\tif e == nil {\n\t\treturn Rules{}, false\n\t}\n\treturn e.rules, true\n}\n\n// Digest is the key a question occupies in the open index, derived from the\n// kind and payload that define it. Exported because it is a pure function of\n// two public strings, and because what it is FOR is worth knowing: a proposer\n// chooses their payload and so chooses their key, which is why the slot sweep\n// rotates rather than always starting at the low end.\nfunc (g *Governor) Digest(kind, payload string) string { return g.digest(kind, payload) }\n\n// Sweep reclaims the slots of proposals that have finished but were never\n// written down.\n//\n// Propose already calls it when the board is full, which is the path that\n// matters. This is the same thing on request, for a caller who would rather\n// free a slot than be refused one — permissionless, like Settle, and for the\n// same reason: it decides nothing, it only writes down what the rules already\n// decided.\nfunc (g *Governor) Sweep() { g.sweep() }\n\n// SetElectorate changes where voting weight comes from, keeping everything\n// else — the adopted kinds, every proposal, the slot cursor.\n//\n// The consuming realm's, like Adopt, and for a narrow purpose: a realm that\n// wraps its ledger to drop idle weight out of the quorum denominator swaps the\n// wrapper in here rather than rebuilding the governor and losing its registry.\n//\n// Only sensible before anything is open. A proposal snapshots the epoch it is\n// weighed at but reads the electorate live, so changing it under an open vote\n// moves the bar the voters were shown — which is the thing per-proposal rules\n// exist to prevent, arrived at from the other side.\nfunc (g *Governor) SetElectorate(e Electorate) {\n\tif e == nil {\n\t\tpanic(\"governor: an electorate is required\")\n\t}\n\tg.voters = e\n}\n\n// Adopt installs a kind as live without a vote.\n//\n// The consuming realm's, and nobody else's — reaching it needs the *Governor,\n// which a realm keeps unexported. That is the same authority the realm already\n// has by holding the engine at all, so this grants nothing new; what it does is\n// make the un-voted path explicit and greppable rather than something a realm\n// improvises by reaching into a tree.\n//\n// For a realm's OWN powers, and for arming a governor in a test. Anything a\n// stranger publishes goes through Offer and an adoption vote, which is the\n// distinction the whole design turns on: publishing code and letting that code\n// hold the governor's authority are different decisions taken by different\n// actors.\n//\n// A realm that exposes this through a crossing function has given governance\n// away. So would exposing Propose without its rules, or Execute without its\n// timelock; the engine cannot stop a consumer publishing the wrong thing, and\n// says so here because this is the one most obviously worth not publishing.\nfunc (g *Governor) Adopt(k Kind, r Rules) {\n\tname := k.Name()\n\tmustBeUsableName(name)\n\tg.mustBeSaneRules(r)\n\tg.kinds.Set(name, \u0026entry{kind: k, rules: r, live: true})\n}\n\n// Render is the front page, or one proposal's page when path names an id.\n//\n// notes are extra lines the consuming realm puts under the token and above the\n// governance — who may mint, most of all, which is the realm's fact rather than\n// the engine's and cannot be inferred from silence.\n//\n// Passed in rather than stored. A stored note is a WRITE, and this is a read: a\n// realm calling SetNotes from its own Render turned reading a page into\n// something that costs storage, which is the defect check-storage exists to\n// catch and did.\nfunc (g *Governor) Render(path, notes string) string { return g.render(path, notes) }\n"},{"name":"keys.gno","body":"// Sorting keys, and nothing else.\n//\n// The clock moved to the ledger, which is what quantises history and so is the\n// only thing that can answer what epoch it is. What is left here is the one\n// encoding decision this realm makes for itself.\npackage governor\n\nimport (\n\tcford32 \"gno.land/p/nt/cford32/v0\"\n)\n\n// enc is a fixed-width, order-preserving key for a number that has to sort.\n//\n// proposal ids are listed in order, so they cannot be decimal strings: \"10\"\n// sorts before \"9\".\nfunc enc(u uint64) string {\n\tb := cford32.PutUint64Lower(u)\n\treturn string(b[:])\n}\n"},{"name":"rules.gno","body":"package governor\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\tufmt \"gno.land/p/nt/ufmt/v0\"\n)\n\n// rulesKind changes what it takes to pass a kind.\n//\n//\tgovern:rules   \"\u003ckind\u003e quorum=2000 threshold=6600 voting=120960 delay=34560 grace=241920 propose=100\"\n//\n// All six terms. ProposeBps was once missing, and it is where a bad value is\n// worst: above a hundred percent nobody can open a question under that kind at\n// all — adopted, unusable, and unfixable by the one kind that changes terms.\n//\n// Fields left out keep their value, so lengthening one delay is a short line.\n//\n// Space-separated key=value because a governance payload has to be readable\n// where it is read — a URL, a wallet's confirmation screen, a forum post. JSON\n// puts a parser between a voter and their intent.\ntype rulesKind struct{}\n\nfunc (k rulesKind) Name() string { return reserved + \"rules\" }\n\n// Describe renders the requested change and NOTHING about the world.\n//\n// Deliberately not \"from 20% to 30%\": that would read live state, so two\n// voters at different heights would see different questions at the same URL,\n// and a proposal that lands in between would silently rewrite what everyone\n// else is voting on.\nfunc (k rulesKind) describe(g *Governor, payload string) string {\n\tname, set, err := parseRules(payload)\n\tif err != nil {\n\t\treturn \"malformed rules change: \" + err.Error()\n\t}\n\tout := \"change the rules for `\" + name + \"`:\"\n\tfor _, f := range set {\n\t\tout += ufmt.Sprintf(\"\\n- %s -\u003e %s\", f.key, f.human())\n\t}\n\treturn out\n}\n\nfunc (k rulesKind) check(g *Governor, payload string) error {\n\tname, set, err := parseRules(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\te := g.entryOf(name)\n\tif e == nil {\n\t\treturn errNoSuchKind\n\t}\n\tif !e.live {\n\t\t// Retuning something not yet adopted would be the same swap the\n\t\t// immutable-name rule closes, moved from the code to the terms: change\n\t\t// a kind's quorum while its adoption vote is open and holders approve\n\t\t// one set of terms and get another. What is not adopted is not the\n\t\t// governor's to tune.\n\t\treturn errNotLive\n\t}\n\t// No check for an empty change set. parseRules refuses a payload with\n\t// fewer than two fields, and every field after the first either parses or\n\t// returns an error, so a nil error here guarantees at least one change. A\n\t// guard would be unreachable, and unreachable defence reads as a case\n\t// somebody thought could happen.\n\tnext := e.rules\n\tapplyRules(\u0026next, set)\n\t// The same sanity the offering path enforces, and literally the same code:\n\t// one function, two callers. A second copy is how the two paths come to\n\t// disagree about what a proposer must hold.\n\treturn saneRules(next)\n}\n\nfunc (k rulesKind) run(g *Governor, dispatch Dispatch, payload string) error {\n\tname, set, err := parseRules(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\te := g.entryOf(name)\n\tif e == nil {\n\t\treturn errNoSuchKind\n\t}\n\tnext := e.rules\n\tapplyRules(\u0026next, set)\n\t// Only the entry changes. Proposals already open kept a COPY of the rules\n\t// they were opened under, so this cannot move the bar under a vote that is\n\t// already being cast.\n\te.rules = next\n\treturn nil\n}\n\ntype field struct {\n\tkey string\n\tn   int64\n}\n\nfunc (f field) human() string {\n\tswitch f.key {\n\tcase \"quorum\", \"threshold\", \"propose\":\n\t\t// A percentage as well as the raw figure: basis points are exact and\n\t\t// nobody reads them. Padded by hand because ufmt has no zero-padding\n\t\t// and its width digits apply to %s alone, so %02d prints 1 rather than\n\t\t// 01 and one basis point renders as 0.1% — ten times the truth.\n\t\treturn ufmt.Sprintf(\"%d bps (%s)\", f.n, pct(f.n))\n\tdefault:\n\t\t// The same rendering the adoption page uses. It printed a bare \"%d\n\t\t// blocks\" while adoptKind printed a duration, so the same field read\n\t\t// two different ways depending on which page you arrived at.\n\t\treturn span(f.n)\n\t}\n}\n\nfunc parseRules(payload string) (string, []field, error) {\n\tparts := strings.Fields(payload)\n\tif len(parts) \u003c 2 {\n\t\treturn \"\", nil, govErr(\"expected a kind name and at least one key=value\")\n\t}\n\tname := parts[0]\n\tvar set []field\n\tfor _, p := range parts[1:] {\n\t\teq := strings.Index(p, \"=\")\n\t\tif eq \u003c= 0 || eq == len(p)-1 {\n\t\t\treturn \"\", nil, govErr(\"expected key=value, got \" + p)\n\t\t}\n\t\tkey, raw := p[:eq], p[eq+1:]\n\t\tswitch key {\n\t\tcase \"quorum\", \"threshold\", \"voting\", \"delay\", \"grace\", \"propose\":\n\t\tdefault:\n\t\t\treturn \"\", nil, govErr(\"unknown field \" + key)\n\t\t}\n\t\tn, err := strconv.ParseInt(raw, 10, 64)\n\t\tif err != nil || n \u003c 0 {\n\t\t\treturn \"\", nil, govErr(\"expected a non-negative number for \" + key)\n\t\t}\n\t\tset = append(set, field{key: key, n: n})\n\t}\n\treturn name, set, nil\n}\n\n// applyRules applies an already-validated change set to r. It cannot fail:\n// parseRules has validated every key and value, so a default/error path here would\n// be unreachable (unreachable defence reads as a case somebody thought could happen).\nfunc applyRules(r *Rules, set []field) {\n\tfor _, f := range set {\n\t\tswitch f.key {\n\t\tcase \"quorum\":\n\t\t\tr.QuorumBps = f.n\n\t\tcase \"threshold\":\n\t\t\tr.ThresholdBps = f.n\n\t\tcase \"voting\":\n\t\t\tr.VotingBlocks = f.n\n\t\tcase \"delay\":\n\t\t\tr.DelayBlocks = f.n\n\t\tcase \"grace\":\n\t\t\tr.GraceBlocks = f.n\n\t\tcase \"propose\":\n\t\t\tr.ProposeBps = f.n\n\t\t}\n\t}\n}\n\n// pct renders basis points as a percentage.\n//\n// Padded by hand because ufmt has no zero-padding and its width digits apply\n// to %s alone, so %02d prints 1 rather than 01 and one basis point reads as\n// 0.1% — ten times the truth, on the line a voter actually reads.\nfunc pct(n int64) string {\n\t// n/(bps/100) is the percentage; bps/100 is 100, so the split is n/100 and\n\t// n%100. Derived from the constant rather than written as 100, so a change\n\t// to the scale cannot leave this rendering the old one.\n\tper := bps / 100\n\tfrac := strconv.FormatInt(n%per, 10)\n\tfor int64(len(frac)) \u003c 2 {\n\t\tfrac = \"0\" + frac\n\t}\n\treturn ufmt.Sprintf(\"%d.%s%%\", n/per, frac)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":"Oi4qXaf6dBLiwD+K/9MxD0xR/K4d+BF0JYHjL0H6cLQVz8strdW7oLFSi2R0+h1LMmakaVcqkH8dx5NKUONhxw=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","package":{"name":"grc20votes","path":"gno.land/p/kourt/grc20votes/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/kourt/grc20votes/v0\"\ngno = \"0.9\"\n"},{"name":"grc20votes.gno","body":"// Package grc20votes is a GRC20-shaped ledger that remembers what every\n// holder's voting power used to be.\n//\n// An analog of OpenZeppelin's ERC20Votes, not a transliteration. Where the\n// EVM's shape exists only because the EVM is what it is — opt-in delegation, a\n// storage model that charges for every extra word forever — gno gets to do the\n// obvious thing instead: delegation defaults to self, and the checkpoint rides\n// in the account record a transfer was going to write anyway.\n//\n// # What a consumer does\n//\n// A realm allocates one Ledger and keeps it in an unexported package variable:\n//\n//\tvar ledger = grc20votes.NewLedger(\"Kourt Governance\", \"COURT\", 6, 720)\n//\n//\tfunc Transfer(cur realm, to address, amount int64) {\n//\t\tif !cur.IsCurrent() {\n//\t\t\tpanic(\"stale realm\")\n//\t\t}\n//\t\tledger.Transfer(cur.Previous().Address(), to, amount)\n//\t}\n//\n// Nothing here takes a `cur realm`, because a /p/ package cannot declare a\n// crossing function. That is not a limitation worked around — it is the right\n// split. Authentication belongs to the realm that has a caller to authenticate;\n// this package is told WHICH address is acting and does the bookkeeping.\n//\n// # Why it is safe to keep this in /p/\n//\n// Every field is unexported and no method hands back an interior pointer, so a\n// consumer holds a *Ledger and nothing else. That matters more here than it\n// looks: /p/-declared types can be named by other /p/ packages, so an exported\n// *account or a method returning one would let a stranger declare a mutator\n// over it — and gno's storage-realm borrow would run that mutator under the\n// CONSUMING realm's authority. See gno-security-guide.md §3(B) and §4; this\n// follows the encapsulation pattern p/demo/tokens/grc20 sets, with the\n// authentication moved out to the realm rather than carried on a teller.\n//\n// The one exception is deliberate: Ledger has no method taking a callback, so\n// there is nothing to launder through. Adding one later would need the\n// parameter type declared in the consuming realm, not here.\npackage grc20votes\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\t\"math/overflow\"\n\t\"strconv\"\n\t\"time\"\n\n\tcheckpoint \"gno.land/p/kourt/checkpoint/v0\"\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\n// Bps is the basis-point scale, named once because it is load-bearing in three\n// places at once here and in any governor reading this ledger: the supply\n// ceiling, a rendered percentage, and a quorum. A scale written out separately\n// in each is a number that can come to disagree with itself.\nconst Bps = int64(10000)\n\n// MaxSupply keeps a tally's arithmetic honest for anything weighing this\n// ledger. A threshold comparison is yes*Bps \u003e= (yes+no)*threshold, and yes*Bps\n// overflows int64 once the supply passes MaxInt64/Bps — a wrapped tally goes\n// negative rather than failing loudly, so a won vote reports as lost.\n//\n// Not a remote bound: six decimals and a billion units in issue is 1e15 base\n// units, already past it. Capped at mint, so a tally needs no overflow checks —\n// roughly 922 million whole tokens at six decimals.\nconst MaxSupply = int64(9223372036854775807) / Bps\n\n// Event names are grc20's exactly (p/demo/tokens/grc20: types.gno\n// TransferEvent, token.gno chain.Emit), so an indexer following\n// Transfer/Approval sees the same stream a stock token produces.\n//\n// What this does NOT do is implement grc20.Teller. The SECURITY note on\n// types.gno's Teller tells consumers to reject anything failing\n// IsCanonicalTeller, so satisfying it would be worthless to a careful caller\n// and misleading to a careless one.\nconst (\n\tmintEvent     = \"Mint\"\n\tburnEvent     = \"Burn\"\n\ttransferEvent = \"Transfer\"\n\tapprovalEvent = \"Approval\"\n\n\t// delegateChangedEvent has no counterpart in grc20 because grc20 has no\n\t// delegation. The name is OpenZeppelin's, since anybody indexing a\n\t// governance token already knows it.\n\tdelegateChangedEvent = \"DelegateChanged\"\n)\n\n// sep joins the two halves of an allowance key. The checkpoint package's\n// separator rather than a byte that happens to match it: two constants with the\n// same value in different files eventually disagree.\nconst sep = checkpoint.Sep\n\n// supplyKey checkpoints the supply like any holder, under a key no address can\n// take. \"~\" is not in the bech32 alphabet and an address always begins with\n// \"g\". Not the separator itself, which the checkpoint package refuses as a key:\n// one containing the separator shares a page range with its neighbours.\nconst supplyKey = \"~supply\"\n\n// account is one holder: what they have, who votes it, and what it was.\n//\n// The votes series is a value field, not a pointer, so changing it dirties this\n// record and nothing else — which is why checkpointing is affordable here.\n//\n// The record outlives a zero balance. The stock ledger removes a holder at zero\n// (grc20 token.gno, led.balances.Remove); doing that here would delete the\n// voting history, and an account oscillating through zero would churn keys.\ntype account struct {\n\tbalance  int64\n\tdelegate address // \"\" means self — see Delegate\n\tvotes    checkpoint.Series\n}\n\n// Ledger is one token's balances and the history of their voting power.\n//\n// Allocated by the consuming realm, which is what makes this work at all: the\n// trees inside carry that realm's storage stamp, so a method here borrows the\n// consumer's authority for the write and the storage is billed to them. A /p/\n// package's own state is frozen after init and could hold none of this.\n// Clock is where this ledger's sense of height comes from. A realm supplies\n// one so its OWN clock governs epochs; nil means the chain's, which is what an\n// ordinary deployment wants.\n//\n// It exists because a realm that can fast-forward its clock for testing cannot\n// fast-forward this package's: a pure package may not import a realm, and a\n// SETTABLE package global would be a capability any caller could take. Bound at\n// construction by the realm that owns the ledger, it is neither.\ntype Clock interface {\n\t// Height is the block height this ledger should quantise epochs by.\n\tHeight() int64\n\n\t// Now is the same block's wall-clock time, unix seconds. The ledger itself\n\t// quantises by HEIGHT and always will — epoch quantisation is what makes the\n\t// anti-flash-loan property structural — but it passes this through to the\n\t// governor on the Electorate interface, where a voting DEADLINE lives.\n\tNow() int64\n}\n\ntype Ledger struct {\n\t// clock is nil on an ordinary deployment; see Clock.\n\tclock    Clock\n\tname     string\n\tsymbol   string\n\tdecimals int\n\tid       string\n\n\t// epochBlocks quantises the clock. Every change inside one epoch coalesces\n\t// into a single update of a record already being written, which is what\n\t// bounds archive growth by wall-clock activity rather than by block\n\t// production — and it makes the anti-flash-loan property an invariant\n\t// rather than a discipline, since a transaction cannot outlive its block\n\t// and a block cannot outlive its epoch.\n\tepochBlocks int64\n\n\taccounts   *bptree.BPTree // address -\u003e *account\n\tallowances *bptree.BPTree // owner + sep + spender -\u003e int64\n\n\t// THIS ARCHIVE IS NEVER TRIMMED, and that is a property of the design rather\n\t// than an omission. checkpoint.Archive has a Trim, the claim's hourly stake\n\t// series uses it, and one line here would look like an ordinary storage\n\t// saving.\n\t//\n\t// Trim's own contract is \"ValueAt exact AT AND ABOVE keepFrom\". Every question\n\t// in the realm above is anchored at a PAST epoch and reads two values from\n\t// here — PastVotes for each voter's ceiling and PastTotal for the bar those\n\t// votes are judged against — so trimming past a live question's epoch makes\n\t// both inexact, independently of each other. That is the numerator and the\n\t// denominator drifting to different instants, which is the defect that\n\t// produced turnout at 200-400% of its own bar and was reverted.\n\t//\n\t// What would have to be true to relax it: keepFrom below the oldest epoch any\n\t// open question is anchored at, which means knowing every open question — a\n\t// fact this package deliberately does not have. check-epoch-coherence arm 12\n\t// pins the call sites meanwhile.\n\tarchive *checkpoint.Archive\n\n\tsupply checkpoint.Series\n\ttotal  int64\n}\n\n// NewLedger creates an empty ledger. The identity is fixed here rather than\n// read from a constant, so one realm can run several and a court can name its\n// own coin.\n//\n// epochBlocks is the quantisation, in blocks. At gno's five-second cadence 720\n// is an hour, which is the figure the rest of this design was measured at.\n// NewLedgerWithClock is NewLedger with the height source named. A realm whose\n// own clock can be fast-forwarded passes itself; everyone else calls NewLedger.\nfunc NewLedgerWithClock(name, symbol string, decimals int, epochBlocks int64, clk Clock) *Ledger {\n\tl := NewLedger(name, symbol, decimals, epochBlocks)\n\tl.clock = clk\n\treturn l\n}\n\nfunc NewLedger(name, symbol string, decimals int, epochBlocks int64) *Ledger {\n\tif name == \"\" || symbol == \"\" {\n\t\tpanic(\"grc20votes: a token needs a name and a symbol\")\n\t}\n\tif decimals \u003c 0 || decimals \u003e 18 {\n\t\tpanic(\"grc20votes: decimals must be between 0 and 18\")\n\t}\n\tif epochBlocks \u003c= 0 {\n\t\tpanic(\"grc20votes: an epoch has to be at least one block long\")\n\t}\n\treturn \u0026Ledger{\n\t\tname: name, symbol: symbol, decimals: decimals,\n\t\t// The pkgpath is not knowable from here, so the id is the symbol\n\t\t// qualified by whatever the consumer wants. A realm that wants the\n\t\t// grc20reg shape passes its own path in the name.\n\t\tid:          symbol,\n\t\tepochBlocks: epochBlocks,\n\t\taccounts:    bptree.NewBPTree32(),\n\t\tallowances:  bptree.NewBPTree32(),\n\t\tarchive:     checkpoint.NewArchive(),\n\t}\n}\n\n// SetID names this token the way an indexer will see it, usually\n// \"gno.land/r/you/realm.SYMBOL\". Called once, at construction time, by the\n// realm that knows its own path — a /p/ package cannot ask.\nfunc (l *Ledger) SetID(id string) { l.id = id }\n\n// ------------------------------------------------------------------ reads --\n\n// The GRC20 reads, with the names that standard uses, answering about NOW.\n//\n// Governance does not read any of these. It asks PastVotes and PastTotal,\n// which answer about a sealed epoch — the distinction the whole design turns\n// on, and the reason these are grouped apart from them.\n\nfunc (l *Ledger) Name() string       { return l.name }\nfunc (l *Ledger) Symbol() string     { return l.symbol }\nfunc (l *Ledger) Decimals() int      { return l.decimals }\nfunc (l *Ledger) ID() string         { return l.id }\nfunc (l *Ledger) TotalSupply() int64 { return l.total }\n\nfunc (l *Ledger) BalanceOf(owner address) int64 {\n\tif a := l.getAccount(owner); a != nil {\n\t\treturn a.balance\n\t}\n\treturn 0\n}\n\nfunc (l *Ledger) Allowance(owner, spender address) int64 {\n\tif v := l.allowances.Get(string(owner) + sep + string(spender)); v != nil {\n\t\treturn v.(int64)\n\t}\n\treturn 0\n}\n\n// VotesOf is voting power right now — the sum of every balance delegated here,\n// including the holder's own unless they delegated it away.\nfunc (l *Ledger) VotesOf(who address) int64 {\n\tif a := l.getAccount(who); a != nil {\n\t\treturn a.votes.Value()\n\t}\n\treturn 0\n}\n\n// DelegateOf is who votes an address's balance. Self unless they said\n// otherwise, which is the default nobody has to remember.\nfunc (l *Ledger) DelegateOf(who address) address {\n\ta := l.getAccount(who)\n\tif a == nil || a.delegate == \"\" {\n\t\treturn who\n\t}\n\treturn a.delegate\n}\n\n// PastVotes is voting power throughout a SEALED epoch. Refusing the current one\n// is the anti-flash-loan property as an invariant rather than a convention: a\n// transaction runs inside one block, a block inside one epoch, and this will\n// not answer for the epoch it is in.\nfunc (l *Ledger) PastVotes(who address, at uint32) int64 {\n\tl.mustBeSealed(at)\n\ta := l.getAccount(who)\n\tif a == nil {\n\t\treturn 0\n\t}\n\treturn a.votes.ValueAt(l.archive, string(who), at)\n}\n\n// PastTotal is every unit in existence during a sealed epoch, which a quorum is\n// a fraction of.\n//\n// Because delegation defaults to self, everyone's PastVotes sums to exactly\n// this. OZ cannot say so: getPastTotalSupply counts all supply while\n// getPastVotes counts only DELEGATED supply, so the denominator exceeds the\n// achievable numerator and quorum is unreachable where few have delegated.\nfunc (l *Ledger) PastTotal(at uint32) int64 {\n\tl.mustBeSealed(at)\n\treturn l.supply.ValueAt(l.archive, supplyKey, at)\n}\n\n// EngagedTotal is what a governor measures quorum against. Every holder is\n// engaged here by definition, since power is self-delegated by default — a\n// realm wanting idle weight dropped out of the denominator wraps this.\nfunc (l *Ledger) EngagedTotal(at uint32) int64 { return l.PastTotal(at) }\n\n// WalkSupply visits the supply's CHANGE points, newest first, and stops early\n// when fn returns true. An epoch in which supply did not move is not a point:\n// a reader forward-fills between them.\n//\n// A WALK, not a per-epoch read. PastTotal answers one epoch and is the wrong\n// shape for drawing a line — a chart over a year of hourly epochs would be\n// 8,760 lookups to find the handful of epochs that actually moved. The archive\n// already stores only the movements, so this hands them over directly and the\n// cost is the number of mints, not the age of the court.\n//\n// Unsealed on purpose, unlike PastTotal: the newest point may be the running\n// epoch, which is exactly the value a live chart wants at its right edge. It\n// may still coalesce before the epoch seals, so it is a reading and not a\n// record.\nfunc (l *Ledger) WalkSupply(fn func(e uint32, v int64) bool) {\n\t// The two hot slots hold the newest points and are not in the archive yet.\n\tif e0 := l.supply.Since(); e0 != 0 {\n\t\tif fn(e0, l.supply.Value()) {\n\t\t\treturn\n\t\t}\n\t}\n\tif e1, prev := l.supply.Prev(); e1 != 0 {\n\t\tif fn(e1, prev) {\n\t\t\treturn\n\t\t}\n\t}\n\tl.archive.WalkDesc(supplyKey, fn)\n}\n\n// WalkAccounts hands every account's address and balance to fn, ascending by\n// address, and stops early when fn returns true.\n//\n// NO NEW STATE. The accounts tree is the ledger's own index and has always held\n// exactly this; what was missing was a way to read it as a set rather than one\n// address at a time. A consumer wanting a holder ranking would otherwise have\n// had to maintain a second index alongside every mint, burn and transfer — a\n// duplicate that can only ever drift out of step with the first.\n//\n// ZERO BALANCES ARE HANDED OVER TOO, deliberately. An account can reach zero and\n// stay in the tree — Remove would drop the delegate and the vote history with\n// it, which is why the balance path never removes — so filtering here would\n// quietly hide accounts that still carry voting state. The caller knows which\n// question it is asking; this one answers \"what does the ledger hold\".\nfunc (l *Ledger) WalkAccounts(fn func(who address, bal int64) bool) {\n\tl.accounts.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\ta, ok := v.(*account)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\treturn fn(address(k), a.balance)\n\t})\n}\n\n// Epoch is now. A proposal snapshots Epoch()-1 and stores it; nothing\n// re-derives it later.\nfunc (l *Ledger) Epoch() uint32 {\n\t// 1-based, so zero can mean \"before this token existed\" — which is what\n\t// lets an untouched series answer correctly without storing anything.\n\treturn uint32(l.Height()/l.epochBlocks) + 1\n}\n\n// Height is this ledger's clock, and the governor's: it is on the Electorate\n// interface so a governor weighing votes uses the same height the ledger\n// quantised its snapshots by. Falling back to the chain when no clock was\n// supplied keeps every existing construction working unchanged.\nfunc (l *Ledger) Height() int64 {\n\tif l.clock != nil {\n\t\treturn l.clock.Height()\n\t}\n\treturn runtime.ChainHeight()\n}\n\n// Now is this ledger's wall clock, forwarded to the governor through the\n// Electorate interface. It is NOT used to quantise anything here: epochs are\n// height-quantised by design (see Clock), and this exists only so a deadline\n// the governor publishes can be a date.\nfunc (l *Ledger) Now() int64 {\n\tif l.clock != nil {\n\t\treturn l.clock.Now()\n\t}\n\treturn time.Now().Unix()\n}\n\n// EpochBlocks is the quantisation this ledger was built with, so a consumer\n// rendering a deadline in hours does not have to be told twice.\nfunc (l *Ledger) EpochBlocks() int64 { return l.epochBlocks }\n\nfunc (l *Ledger) mustBeSealed(at uint32) {\n\tif at == 0 || at \u003e= l.Epoch() {\n\t\tpanic(\"grc20votes: that epoch has not been sealed yet\")\n\t}\n}\n\n// ----------------------------------------------------------------- writes --\n//\n// Every one of these takes the acting address rather than reading it. The realm\n// above has the `cur realm` and is the only thing that can authenticate; being\n// handed an address here is what keeps this package free of a capability it\n// would have no way to check.\n\n// Transfer moves `from`'s own tokens.\nfunc (l *Ledger) Transfer(from, to address, amount int64) {\n\tl.move(from, to, amount)\n}\n\n// Approve lets a spender move some of the owner's balance.\nfunc (l *Ledger) Approve(owner, spender address, amount int64) {\n\tmustBeValid(spender)\n\tif amount \u003c 0 {\n\t\tpanic(\"grc20votes: negative allowance\")\n\t}\n\tkey := string(owner) + sep + string(spender)\n\tif amount == 0 {\n\t\tl.allowances.Remove(key)\n\t} else {\n\t\tl.allowances.Set(key, amount)\n\t}\n\tchain.Emit(approvalEvent,\n\t\t\"token\", l.id,\n\t\t\"owner\", owner.String(),\n\t\t\"spender\", spender.String(),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n}\n\n// TransferFrom spends an allowance.\nfunc (l *Ledger) TransferFrom(spender, from, to address, amount int64) {\n\tkey := string(from) + sep + string(spender)\n\tallowed := l.Allowance(from, spender)\n\tif allowed \u003c amount {\n\t\tpanic(\"grc20votes: allowance exceeded\")\n\t}\n\t// Debited before the move. Defence in depth rather than a live fix: nothing\n\t// in move calls out, so today the order is unobservable and no test can\n\t// tell the difference. It is written this way for the day something here\n\t// does call out, when it becomes the difference between spending an\n\t// allowance once and twice.\n\tif rest := allowed - amount; rest == 0 {\n\t\tl.allowances.Remove(key)\n\t} else {\n\t\tl.allowances.Set(key, rest)\n\t}\n\tl.move(from, to, amount)\n}\n\n// Mint creates tokens. This package holds no minter: who may call it is the\n// consuming realm's rule, because that is where the caller is known.\nfunc (l *Ledger) Mint(to address, amount int64) {\n\tmustBeValid(to)\n\tmustBePositive(amount)\n\n\tnext, ok := overflow.Add64(l.total, amount)\n\tif !ok || next \u003e MaxSupply {\n\t\tpanic(\"grc20votes: supply would exceed what a tally can weigh without overflowing\")\n\t}\n\tl.total = next\n\tl.supply.SetAt(l.archive, supplyKey, l.Epoch(), next)\n\n\ta := l.openAccount(to)\n\ta.balance += amount\n\tl.addVotes(delegateeOf(to, a), amount)\n\n\t// Both events, and the Transfer is the one that matters to anybody else.\n\t//\n\t// grc20 declares MintEvent and BurnEvent and emits neither: its Mint sends\n\t// a TRANSFER with an empty `from` and its Burn one with an empty `to`. That\n\t// is the ERC20 convention, and how anything written against the standard\n\t// reconstructs balances — sum the Transfers, treat the empty counterparty\n\t// as the supply change. Emitting only \"Mint\" matches the names while\n\t// breaking what they are for.\n\t//\n\t// \"Mint\" is kept because it says plainly what happened. Anything totalling\n\t// supply should total Transfers and not both, which is safe for a standard\n\t// indexer since the standard never emits \"Mint\".\n\tchain.Emit(transferEvent,\n\t\t\"token\", l.id,\n\t\t\"from\", \"\",\n\t\t\"to\", to.String(),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\tchain.Emit(mintEvent,\n\t\t\"token\", l.id,\n\t\t\"to\", to.String(),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n}\n\n// Burn destroys `from`'s own tokens.\nfunc (l *Ledger) Burn(from address, amount int64) {\n\tmustBePositive(amount)\n\ta := l.getAccount(from)\n\tif a == nil || a.balance \u003c amount {\n\t\tpanic(\"grc20votes: insufficient balance\")\n\t}\n\ta.balance -= amount\n\tl.addVotes(delegateeOf(from, a), -amount)\n\n\tl.total -= amount\n\tl.supply.SetAt(l.archive, supplyKey, l.Epoch(), l.total)\n\n\t// The other half of the convention: a burn is a Transfer to nowhere. See\n\t// the note in Mint for why both events go out.\n\tchain.Emit(transferEvent,\n\t\t\"token\", l.id,\n\t\t\"from\", from.String(),\n\t\t\"to\", \"\",\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\tchain.Emit(burnEvent,\n\t\t\"token\", l.id,\n\t\t\"from\", from.String(),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n}\n\n// Delegate points an address's voting power at someone else.\n//\n// Self-delegation is the DEFAULT, which OZ cannot afford: there it would put\n// two SSTOREs on every transfer forever, so it is opt-in and everybody is\n// surprised by it once. Here the checkpoint lives in a record the transfer\n// already writes.\n//\n// One hop, not transitive: if A delegates to B and B to C, A's weight sits with\n// B. Transitivity invites cycles and unbounded walks and buys nothing a second\n// call cannot.\nfunc (l *Ledger) Delegate(from, to address) {\n\tmustBeValid(to)\n\n\tif l.getAccount(from) == nil \u0026\u0026 to == from {\n\t\t// Already self-delegated, because everybody is: DelegateOf reads a\n\t\t// missing account and a blank delegate the same way, so a record here\n\t\t// buys a key to store a fact true of every address that never existed.\n\t\t//\n\t\t// An address holding nothing CAN still name somebody else, and that has\n\t\t// to persist for when it is funded. What it cannot usefully do is name\n\t\t// itself.\n\t\treturn\n\t}\n\ta := l.openAccount(from)\n\n\twas := delegateeOf(from, a)\n\tif to == from {\n\t\ta.delegate = \"\" // normalised, so \"self\" has one representation\n\t} else {\n\t\ta.delegate = to\n\t}\n\tnow := delegateeOf(from, a)\n\tif was == now {\n\t\treturn\n\t}\n\tl.addVotes(was, -a.balance)\n\tl.addVotes(now, a.balance)\n\n\t// The only state change here that nothing else reveals. Delegating moves\n\t// power without moving a coin, so an indexer sees a holder's weight vanish\n\t// with nothing to explain it, and the only alternative is polling\n\t// DelegateOf for every address anyone has heard of.\n\t//\n\t// OZ also emits DelegateVotesChanged on every power change, which here\n\t// would fire on every transfer, mint and burn — for something a caller can\n\t// ask: VotesOf answers now, PastVotes for any sealed epoch. Emitting it\n\t// would be transliteration.\n\tchain.Emit(delegateChangedEvent,\n\t\t\"token\", l.id,\n\t\t\"delegator\", from.String(),\n\t\t\"fromDelegate\", was.String(),\n\t\t\"toDelegate\", now.String(),\n\t)\n}\n\n// --------------------------------------------------------------- innards --\n\nfunc (l *Ledger) move(from, to address, amount int64) {\n\tmustBeValid(to)\n\tmustBePositive(amount)\n\tsrc := l.getAccount(from)\n\tif src == nil || src.balance \u003c amount {\n\t\tpanic(\"grc20votes: insufficient balance\")\n\t}\n\tdst := l.openAccount(to)\n\n\tsrc.balance -= amount\n\tdst.balance += amount\n\n\t// Resolved once each. When both sides vote their own balance — the\n\t// overwhelmingly common case — these are the records already in hand, so a\n\t// checkpointed transfer dirties the same two objects a plain one would. The one\n\t// exception is a side's FIRST write in a new epoch, which also rolls one point\n\t// into the archive (one added page); amortised across the epoch's writes that\n\t// is ~nothing, but it is not literally two objects at the boundary.\n\tl.addVotes(delegateeOf(from, src), -amount)\n\tl.addVotes(delegateeOf(to, dst), amount)\n\n\tchain.Emit(transferEvent,\n\t\t\"token\", l.id,\n\t\t\"from\", from.String(),\n\t\t\"to\", to.String(),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n}\n\nfunc delegateeOf(self address, a *account) address {\n\tif a.delegate == \"\" {\n\t\treturn self\n\t}\n\treturn a.delegate\n}\n\n// addVotes moves voting power and checkpoints it.\nfunc (l *Ledger) addVotes(who address, delta int64) {\n\tif delta == 0 {\n\t\treturn\n\t}\n\ta := l.openAccount(who)\n\tnext := a.votes.Value() + delta\n\tif next \u003c 0 {\n\t\tpanic(\"grc20votes: voting power would go negative\")\n\t}\n\ta.votes.SetAt(l.archive, string(who), l.Epoch(), next)\n}\n\nfunc (l *Ledger) getAccount(who address) *account {\n\tif v := l.accounts.Get(string(who)); v != nil {\n\t\treturn v.(*account)\n\t}\n\treturn nil\n}\n\nfunc (l *Ledger) openAccount(who address) *account {\n\tif a := l.getAccount(who); a != nil {\n\t\treturn a\n\t}\n\ta := \u0026account{}\n\tl.accounts.Set(string(who), a)\n\treturn a\n}\n\nfunc mustBeValid(a address) {\n\tif !a.IsValid() {\n\t\tpanic(\"grc20votes: not an address\")\n\t}\n}\n\nfunc mustBePositive(amount int64) {\n\tif amount \u003c= 0 {\n\t\tpanic(\"grc20votes: amount must be positive\")\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":"sUtGozNF43IQUoBXw5qxFJ6nT/rgM95G60Un6Flb0BMQlFJxnaZd2VRd7GdEPJlerMr2rQoSD3Aju0kOv3aw4g=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","package":{"name":"twap","path":"gno.land/p/kourt/twap/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/kourt/twap/v0\"\ngno = \"0.9\"\n"},{"name":"twap.gno","body":"// Package twap is a fixed-window trailing average of an integer series: cheap to\n// keep, cheap to read, and hard to move with a one-block spike.\n//\n// It answers one question — \"what has this number averaged over the last W?\" —\n// which is what an on-chain market wants from a price oracle and what a token\n// vote wants from a supply-or-turnout figure: a value that a flash spike cannot\n// shift, because moving the average requires HOLDING the pushed value for a real\n// fraction of the window.\n//\n// # Not checkpoint\n//\n// The sibling p/kourt/checkpoint answers a different question — \"what was\n// this value at sealed epoch N?\" — from unbounded, paged history. This is a\n// bounded rolling window with no archive: the last N buckets and nothing older.\n// Different query, different structure, no overlap.\n//\n// # A value, not an object\n//\n// A Ring is a plain value the caller stores inline on a record it was going to\n// write anyway — a market's own row. It is never a heap object of its own, and\n// never held by pointer in realm state: every method takes a Ring by value and\n// returns an updated one. The reason to keep it a value is purely GAS, not\n// safety: gno charges per object touched, so a Ring that rides its host's write\n// costs nothing extra to keep, whereas a *Ring would be a second object (a second\n// key) — and it would buy nothing, since every method is a value receiver that\n// hands back a copy anyway. There is no mutator to borrow and so no security\n// question here at all; the value shape is a cost decision.\n//\n//\tr = r.Observe(height, price)   // store r back onto your own record\n//\tavg, ok := r.Average(height, window)\n//\n// # Buckets, and why a spike does not move the average\n//\n// Time is quantised into fixed-width buckets. Each bucket remembers the last\n// value seen in it; a bucket with no observation carries the previous value\n// forward, so a quiet series reads as \"unchanged\", not as a gap. The average\n// over a window of W is the mean of the W/width most recent buckets.\n//\n// A spike therefore lands in at most one bucket — one part in W/width of the\n// average — and only if it is still the last value in that bucket when the bucket\n// closes. To move the average by a fraction f of the series' range you must keep\n// it moved across about f·(W/width) buckets, i.e. hold the pushed value for f of\n// the whole window against everyone trading back. One block is ~1/(W/width) of\n// that, which for a week of hourly buckets is under a thousandth of a spike.\n//\n// # The freshness contract\n//\n// That guarantee holds only while observations keep arriving. An empty bucket\n// carries the last value forward, so if the caller STOPS observing, the last\n// value — a spike included — persists across the whole window and the average\n// becomes that single value, still reported mature. So the caller must Observe on\n// every change to the tracked quantity, OR Observe at the height it later reads.\n// StaleBy reports how far a read has drifted from the newest observation, for a\n// caller that cannot guarantee the former. `mature` means \"the window is\n// covered\", never \"the data is recent\".\npackage twap\n\nimport \"math/overflow\"\n\n// Ring is a fixed-window trailing average. The zero value is not usable; build\n// one with New.\n//\n// bpp is bytes per sample: 1 for a 0..255 value such as a percentage price, up\n// to 8 for a full int64. Samples are packed big-endian into buf so the whole\n// history is one []byte-shaped field rather than N times the 40 bytes gno spends\n// on a slice element of any wider type.\ntype Ring struct {\n\twidth  int64  // bucket width, in the caller's height units\n\tn      int    // number of buckets\n\tbpp    int    // bytes per sample\n\thead   int    // ring index of the newest bucket\n\tbase   int64  // bucket number (height/width) at head\n\tlast   int64  // most recent value, carried across empty buckets\n\tfilled int    // buckets ever written, for maturity\n\tbuf    string // n*bpp packed big-endian samples\n}\n\n// New builds an empty ring of n buckets, each width height-units wide, storing\n// bpp bytes per sample.\n//\n// The window a caller later asks Average for must be at most n*width, or the ring\n// cannot cover it and the read is reported immature. Size n to the longest window\n// you will read.\n//\n// width is the resolution-versus-cost trade: a finer width puts more buckets in a\n// window (more storage, a longer Average scan) but resists a spike that is held\n// for less real time; a coarser width is cheaper and blunter. n*width fixes the\n// span, so the two are chosen together — e.g. a week resisted at hourly grain is\n// width=1h, n=168.\nfunc New(width int64, n, bpp int) Ring {\n\tmustSane(width, n, bpp)\n\treturn Ring{\n\t\twidth: width, n: n, bpp: bpp,\n\t\thead: 0, base: 0, last: 0, filled: 0,\n\t\tbuf: string(make([]byte, n*bpp)),\n\t}\n}\n\n// Load reconstructs a ring from a previously stored header and its buf.\n//\n// The three shape numbers are the caller's own constants; only buf is data, so a\n// caller that keeps width/n/bpp as package constants stores just the head fields\n// and the bytes.\n//\n// Load validates SHAPE — buf length, and head/filled in range — and panics on a\n// mismatch. It TRUSTS base and last: a wrong value there only skews a later\n// average (never reads out of bounds), so pass back exactly what the accessors\n// returned rather than hand-built numbers.\nfunc Load(width int64, n, bpp int, head int, base, last int64, filled int, buf string) Ring {\n\tmustSane(width, n, bpp)\n\tif len(buf) != n*bpp {\n\t\tpanic(\"twap: buf length does not match n*bpp\")\n\t}\n\tif head \u003c 0 || head \u003e= n || filled \u003c 0 || filled \u003e n {\n\t\tpanic(\"twap: head or filled out of range\")\n\t}\n\treturn Ring{width: width, n: n, bpp: bpp, head: head, base: base, last: last, filled: filled, buf: buf}\n}\n\n// Observe records value as of height and returns the updated ring.\n//\n// Height must not go backwards below the newest bucket already written; a chain\n// height only ever increases, and a caller that resets it (a test harness) is the\n// one case this refuses, loudly, because a backwards write would corrupt every\n// later average.\nfunc (r Ring) Observe(height, value int64) Ring {\n\tif r.n == 0 {\n\t\tpanic(\"twap: zero-value ring; use New\")\n\t}\n\tbn := height / r.width\n\tb := []byte(r.buf)\n\tif r.filled == 0 {\n\t\t// Seed on the FIRST real observation, wherever it lands. A market is\n\t\t// created at some height H and its first Observe is at H, not at 0 — so\n\t\t// base must start at H's bucket, not at zero. Seeding at zero and then\n\t\t// advancing to H would carry the zero-value `last` into every bucket in\n\t\t// between and count them in `filled`, and the ring would then report a\n\t\t// week of history averaging zero as mature and true when the only value\n\t\t// ever seen was, say, 50. That is a wrong average handed to a quorum, and\n\t\t// it is exactly the case no test with a height-0 start ever reaches.\n\t\tr.base = bn\n\t\tput(b, r.head*r.bpp, r.bpp, value)\n\t\tr.filled, r.last, r.buf = 1, value, string(b)\n\t\treturn r\n\t}\n\tif bn \u003c r.base {\n\t\tpanic(\"twap: height went backwards\")\n\t}\n\tif bn == r.base {\n\t\t// Same bucket: overwrite its representative value.\n\t\tput(b, r.head*r.bpp, r.bpp, value)\n\t\tr.last = value\n\t\tr.buf = string(b)\n\t\treturn r\n\t}\n\t// Advance across the gap, carrying `last` into every skipped bucket and\n\t// `value` into the newest. More than n steps just laps the ring, so cap the\n\t// work at n — the older laps are overwritten anyway.\n\tsteps := bn - r.base\n\tif steps \u003e int64(r.n) {\n\t\tsteps = int64(r.n)\n\t}\n\tfor i := int64(0); i \u003c steps; i++ {\n\t\tr.head++\n\t\tif r.head == r.n {\n\t\t\tr.head = 0\n\t\t}\n\t\tfill := r.last\n\t\tif i == steps-1 {\n\t\t\tfill = value\n\t\t}\n\t\tput(b, r.head*r.bpp, r.bpp, fill)\n\t\tif r.filled \u003c r.n {\n\t\t\tr.filled++\n\t\t}\n\t}\n\tr.base = bn\n\tr.last = value\n\tr.buf = string(b)\n\treturn r\n}\n\n// Average returns the trailing average over [height-window, height] and whether\n// the ring held enough history to cover the whole window.\n//\n// mature means the window is COVERED by real buckets — not that those buckets are\n// RECENT. An empty tail carries the last value forward (see StaleBy and the\n// freshness contract), so a lone spike that is the last observation before a quiet\n// spell fills the window and still reads mature. A caller acting on the average\n// against manipulation — gating a vote, pricing a payout — must BOTH refuse to act\n// while mature is false AND keep the read fresh: Observe at (or near) the height\n// it reads, or gate on StaleBy. Reading is pure — a transient decode, no object\n// touched — so it is safe inside a Render.\nfunc (r Ring) Average(height, window int64) (avg int64, mature bool) {\n\tif r.n == 0 {\n\t\tpanic(\"twap: zero-value ring; use New\")\n\t}\n\tif window \u003c r.width {\n\t\twindow = r.width\n\t}\n\t// want is how many buckets the window asks for, UNCAPPED. Maturity is measured\n\t// against it, so a window wider than the ring can ever hold (want \u003e n) reads\n\t// immature — rather than silently returning the ring's shorter average stamped\n\t// as the requested one. k is want capped to what the ring can scan.\n\twant := int(window / r.width)\n\tk := want\n\tif k \u003e r.n {\n\t\tk = r.n\n\t}\n\tbn := height / r.width\n\tvar sum int64\n\tcount := 0\n\tfor i := 0; i \u003c k; i++ {\n\t\tbkt := bn - int64(i)\n\t\tif bkt \u003c 0 || bkt \u003c= r.base-int64(r.filled) {\n\t\t\tbreak // older than the ring has ever held real data\n\t\t}\n\t\tvar v int64\n\t\tif bkt \u003e r.base {\n\t\t\tv = r.last // beyond the last observation: value has persisted\n\t\t} else {\n\t\t\tidx := r.head - int(r.base-bkt)\n\t\t\tfor idx \u003c 0 {\n\t\t\t\tidx += r.n\n\t\t\t}\n\t\t\tv = get(r.buf, idx*r.bpp, r.bpp) // read the string directly; no []byte copy\n\t\t}\n\t\t// Checked, not assumed. Values are non-negative, so a wrapped sum goes\n\t\t// negative and would hand a quorum a negative \"average\"; refuse loudly\n\t\t// instead. Trips only when the ring holds more large buckets than fit in\n\t\t// int64 (e.g. per-minute buckets at the supply cap) — size the ring or\n\t\t// narrow the value range.\n\t\tnext, ok := overflow.Add64(sum, v)\n\t\tif !ok {\n\t\t\tpanic(\"twap: window sum overflowed int64\")\n\t\t}\n\t\tsum = next\n\t\tcount++\n\t}\n\tif count == 0 {\n\t\treturn 0, false\n\t}\n\treturn sum / int64(count), count == want \u0026\u0026 r.filled \u003e= want\n}\n\n// Head, Base, Last, Filled, Bytes expose the fields a caller stores alongside buf\n// to reconstruct the ring with Load.\nfunc (r Ring) Head() int     { return r.head }\nfunc (r Ring) Base() int64   { return r.base }\nfunc (r Ring) Last() int64   { return r.last }\nfunc (r Ring) Filled() int   { return r.filled }\nfunc (r Ring) Bytes() string { return r.buf }\n\n// StaleBy reports how many buckets the read height lies beyond the newest\n// observation. Zero means the newest real value sits in height's own bucket; a\n// larger number means the window a caller is about to Average is that many buckets\n// of carried-forward value rather than fresh data. Because `mature` only says the\n// window is covered, a caller acting against manipulation should require StaleBy\n// to be small — ideally 0, i.e. Observe at the height it reads (see the freshness\n// contract on the package).\nfunc (r Ring) StaleBy(height int64) int64 {\n\tif r.n == 0 {\n\t\tpanic(\"twap: zero-value ring; use New\")\n\t}\n\tbn := height / r.width\n\tif bn \u003c= r.base {\n\t\treturn 0\n\t}\n\treturn bn - r.base\n}\n\nfunc mustSane(width int64, n, bpp int) {\n\tif width \u003c= 0 {\n\t\tpanic(\"twap: width must be positive\")\n\t}\n\tif n \u003c= 0 {\n\t\tpanic(\"twap: n must be positive\")\n\t}\n\tif bpp \u003c 1 || bpp \u003e 8 {\n\t\tpanic(\"twap: bpp must be 1..8\")\n\t}\n}\n\n// put writes v big-endian into b[off:off+bpp]. The caller guarantees v fits in\n// bpp bytes; a value wider than the sample width is a caller bug, and truncating\n// it silently is the one behaviour this must not have — so the top bytes are\n// asserted zero.\nfunc put(b []byte, off, bpp int, v int64) {\n\tif v \u003c 0 {\n\t\tpanic(\"twap: negative value\")\n\t}\n\tif bpp \u003c 8 \u0026\u0026 v\u003e\u003e(uint(bpp)*8) != 0 {\n\t\tpanic(\"twap: value does not fit in bpp bytes\")\n\t}\n\tfor i := bpp - 1; i \u003e= 0; i-- {\n\t\tb[off+i] = byte(v \u0026 0xff)\n\t\tv \u003e\u003e= 8\n\t}\n}\n\n// get reads a big-endian sample straight from the buf STRING, so a read never\n// allocates a []byte copy. Only Observe needs the mutable []byte (for put); a\n// read (Average) is the hot path and stays allocation-free.\nfunc get(s string, off, bpp int) int64 {\n\tvar v int64\n\tfor i := 0; i \u003c bpp; i++ {\n\t\tv = (v \u003c\u003c 8) | int64(s[off+i])\n\t}\n\treturn v\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":"CgIsIBaws3CTQUUu2mklKzdt3gwkO5c/oS4pi5kv8o0LON38ALjzwgZSacJlhlAlUEKYJd2ruh4/1zoPjLqUkA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","package":{"name":"sanitize","path":"gno.land/p/nt/markdown/sanitize/v0","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/markdown/sanitize/v0\"\ngno = \"0.9\"\n"},{"name":"sanitize.gno","body":"// Package sanitize provides input-cleaning primitives and safe-emit\n// builders for each markdown lexical slot. Realm authors wrap user-\n// supplied strings with these helpers before flowing them into rendered\n// markdown output. Each helper targets one specific slot (link text,\n// heading text, URL href, table cell, HTML attribute, fenced code block,\n// blockquote, footnote definition, link-reference definition, etc.) and\n// neutralizes the bytes that would otherwise let user content break out\n// of that slot or inject new top-level structure.\n//\n// Pick the right helper from the table under \"Picking the right helper\"\n// below, then wrap each user-supplied argument exactly once at the call\n// site (see \"The audit rule\").\n//\n// # Wrap once\n//\n// Most escapers and safe-emit builders in this package are NOT\n// idempotent — applying them twice re-escapes bytes the first pass\n// added (`\\*` becomes `\\\\\\*`, `\u0026amp;` becomes `\u0026amp;amp;`, a fenced\n// block gets re-fenced). Wrap each user-derived string with at most\n// one sanitize.* call. Block and BlockRich are exceptions —\n// idempotent by design — but the at-most-once rule is still the\n// safest default. See the \"Idempotence classes\" enumeration below\n// for the full breakdown.\n//\n// Some markdown-builder packages (e.g. p/moul/md) sanitize the args of\n// specific helpers internally — see each builder's package doc for the\n// per-helper contract. If the builder sanitizes for you, pass the raw\n// user input; if it doesn't, wrap the input with the right sanitize.*\n// helper at the call site.\n//\n// # Picking the right helper\n//\n// Match the helper to the slot the user content lands in:\n//\n//\tslot                                       helper\n//\t-------------------------------------------------------------\n//\t[text](url)                                InlineText (text)\n//\t# Heading text                             InlineText\n//\t**bold** _italic_                          InlineText\n//\t![alt](src)                                InlineText (alt)\n//\t\u003e [!NOTE] one-line title                   InlineText\n//\tmulti-paragraph post body                  Block\n//\tmulti-paragraph post body w/ rich block    BlockRich\n//\t  structure (headings, lists, tables, etc.)\n//\tmulti-line blockquote (`\u003e ` prefixed)      Blockquote\n//\tmulti-line blockquote w/ rich block body   BlockquoteRich\n//\t[text](url \"title\")                        LinkTitle (title)\n//\t| cell |                                   TableCell\n//\t\u003cgno-card caption=\"X\"\u003e                     HTMLEscape\n//\t\u003ch5\u003eX\u003c/h5\u003e                                 HTMLEscape\n//\tany URL going into ](X)                    URL\n//\tany image src going into (X)               ImageURL\n//\t`inline code` inside running prose         InlineCode\n//\tmulti-line fenced code block               CodeBlock\n//\tmulti-line fenced code with language tag   LanguageCodeBlock\n//\t[^name]: footnote body                     FootnoteDefinition\n//\t[label]: url \"title\" reference def         LinkReferenceDefinition\n//\tr/sys/users handle                         UserName       (validator)\n//\tg1.../gpub1... etc.                        BechString     (validator)\n//\tfootnote / LRD label / {#id} anchor name   FootnoteLabel  (validator)\n//\tfenced-code language tag                   LanguageName   (validator)\n//\tprefix arg to md.Nested                    NestedPrefix   (validator)\n//\n// # Invariants\n//\n// All helpers in this package are panic-free for any string input and\n// run in O(len(input)) time with bounded allocation.\n//\n// Idempotence classes:\n//\n//\tIdempotent (calling twice == calling once):\n//\t  StripBidiAndZeroWidth, NormalizeBreaks\n//\t  UserName, BechString, FootnoteLabel, LanguageName, NestedPrefix\n//\t  URL, ImageURL              (accept→identity; reject→\"\")\n//\t  Block                      (bracket walker treats \\[/\\] as ordinary;\n//\t                              line-leader escapes don't re-fire on\n//\t                              already-escaped `\\#` etc.)\n//\t  BlockRich                  (TrimLeft/TrimRight + \"\\n\\n\" wrap is stable)\n//\n//\tNOT idempotent — never wrap an already-sanitized string:\n//\t  InlineText, LinkTitle, TableCell   (re-escape backslashes)\n//\t  HTMLEscape                         (re-escapes `\u0026` → `\u0026amp;`)\n//\t  Blockquote, BlockquoteRich         (re-prefixes `\u003e `, nesting the quote each pass)\n//\t  InlineCode, CodeBlock,\n//\t  LanguageCodeBlock                  (wrap with a fence — calling twice double-wraps)\n//\t  FootnoteDefinition,\n//\t  LinkReferenceDefinition            (compose Block/InlineText/URL internally —\n//\t                                      passing already-sanitized strings double-escapes)\n//\n//\tCodeFence is pure: same inputs always give the same output.\n//\n// Validators (UserName / BechString / FootnoteLabel / LanguageName /\n// NestedPrefix) return either the cleaned input verbatim or \"\". They\n// never partially-sanitize: if the input doesn't match the slot's\n// charset/shape, the answer is rejection.\n//\n// # Composition rules\n//\n// Direct sanitize use (when emitting markdown without a builder package):\n//\n//\tout := \"# \" + sanitize.InlineText(userTitle) + \"\\n\\n\" +\n//\t       sanitize.Block(userBody)\n//\tout += sanitize.Blockquote(userQuote)\n//\tout += sanitize.LanguageCodeBlock(realmLang, userCode)\n//\n// Use with a builder package (e.g. p/moul/md): pass raw user input to\n// the builder helpers that sanitize internally — do NOT pre-wrap with\n// sanitize.*, or the input gets double-escaped (escapers are not\n// idempotent). See the builder's package doc for the per-helper\n// contract. For example, with p/moul/md:\n//\n//\tmd.Blockquote(userProse)                 // good — md.Blockquote sanitizes\n//\tmd.LanguageCodeBlock(realmLang, userCode) // good — sanitizes both args\n//\tmd.Link(userText, userURL)               // good — sanitizes both slots\n//\n//\tmd.Blockquote(sanitize.Block(userProse))         // BAD: double-wrap\n//\tmd.Link(sanitize.InlineText(t), sanitize.URL(u)) // BAD: double-wrap\n//\n// Wrong (across all callers):\n//\n//\tsanitize.InlineText(sanitize.InlineText(s))   double-wrap (re-escape)\n//\tsanitize.TableCell(sanitize.InlineText(s))    TableCell already calls InlineText\n//\tsanitize.URL(sanitize.InlineText(href))       inline-escape backslash-escapes `.` `-` `_`\n//\t                                              inside the URL, corrupting the host/path\n//\tsanitize.Blockquote(sanitize.Blockquote(s))   double-wrap — outer would escape the\n//\t                                              inner `\u003e ` prefixes\n//\tsanitize.Block(sanitize.BlockRich(s))         double-sanitize — strict Block re-escapes\n//\t                                              the markers BlockRich preserved (headings,\n//\t                                              lists, tables); BlockRich's rich structure\n//\t                                              renders as literal text after Block escapes\n//\t                                              its line-leaders\n//\tsanitize.BlockRich(sanitize.Block(s))         pointless double-sanitize — Block already\n//\t                                              escaped every line-leader to `\\#`/`\\\u003e`/etc.;\n//\t                                              BlockRich preserves the backslash escapes\n//\t                                              as visible artifacts in user prose\n//\tsanitize.Blockquote(sanitize.BlockRich(s))    double-sanitize — Blockquote's Block step\n//\t                                              re-escapes the markers BlockRich preserved\n//\tsanitize.BlockRich(sanitize.Blockquote(s))    nonsense — Blockquote already line-prefixed\n//\t                                              with `\u003e `; BlockRich expects raw user content\n//\tsanitize.BlockquoteRich(sanitize.BlockRich(s)) double-wrap — Rich + Rich nests twice\n//\tsanitize.BlockRich(sanitize.TableCell(s))     wrong slot — use TableCell for cell content,\n//\t                                              BlockRich for multi-paragraph block content\n//\tsanitize.TableCell(multiParagraphProse)       newlines fold to space silently; use a\n//\t                                              non-table layout for multi-paragraph text\n//\n// # Threat model\n//\n// Sanitizers in this package defend against:\n//\n//   - bidi/zero-width injection: invisible characters that make\n//     displayed text disagree with stored bytes (e.g. an address `g1abc...`\n//     that renders as `g1xyz...`, or a username that visually collides\n//     with another). Stripped by StripBidiAndZeroWidth, which runs as\n//     the first step of every text-shaped helper.\n//   - line-ending homoglyphs: CR-only and Unicode separators\n//     (U+0085 NEL, U+2028, U+2029) that some renderers treat as line\n//     breaks. Folded uniformly.\n//   - markdown-structure injection: user content opening a heading,\n//     blockquote, list, code fence, link-reference def, setext underline,\n//     gnoweb extension delimiter, or GFM table row at document level.\n//     Strict Block escapes the line-leading `|` of any GFM table row so\n//     user content cannot inject `\u003ctable\u003e`-shaped structure; permissive\n//     BlockRich preserves table rows so authors can compose `\u003ctable\u003e`\n//     elements (gnoweb loads extension.Table per render_config.go).\n//   - HTML block type 1-5 absorption: CommonMark §4.6 HTML block types 1\n//     (`\u003cscript\u003e`, `\u003cpre\u003e`, `\u003cstyle\u003e`, `\u003ctextarea\u003e`), 2 (`\u003c!--`), 3\n//     (`\u003c?`), 4 (`\u003c!UPPER`), and 5 (`\u003c![CDATA[`) do NOT close on a blank\n//     line — they only close on a type-specific token (`\u003c/tag\u003e`, `--\u003e`,\n//     `?\u003e`, `\u003e`, `]]\u003e`) or EOF. Without a defense, user content opening\n//     any of these would swallow realm chrome appended afterward. Both\n//     Block and BlockRich line-escape the openers (prepend `\\`) so the\n//     block never opens; this defense is unconditional in both modes.\n//     Types 6 and 7 close on a blank line, so BlockRich's `\\n\\n`\n//     paragraph envelope already bounds them and no escape is needed.\n//   - realm-discipline boundary (caller's responsibility, not enforced):\n//     callers should emit realm chrome at flush-left column 0 around\n//     `BlockRich(user)`. Indented chrome (4+ leading spaces, list-item\n//     continuations, footnote-definition body, or an unclosed Type 1\n//     HTML tag in realm chrome before the call) can extend across blank\n//     lines into user content or vice versa. The sanitizer cannot\n//     defend against malformed realm chrome — only against user input.\n//   - footnote / link-reference namespace pollution: user content\n//     containing `[^name]` or `[text][label]` syntax that would otherwise\n//     resolve against realm-defined footnote definitions or link\n//     reference definitions elsewhere on the page. Block escapes the\n//     opening `[` in both shapes.\n//   - reference-link / footnote-ref / shortcut-ref collisions:\n//     `[text][label]`, `[^name]`, and bare `[label]` shortcut forms\n//     are ALL neutralized by Block's bracket walk, which preserves\n//     only inline `[text](url)` and `![alt](src)` syntax — everything\n//     else has both `[` and `]` backslash-escaped, so the parser sees\n//     literal text and can't resolve against realm-defined LRDs or\n//     footnote definitions.\n//   - multi-line LRD evasion: Block's walker recognises `[lab\\nel]: url`\n//     across newlines (single `\\n` OK, blank line aborts) and strips\n//     the whole region. `\\]` inside the label is honored as an escaped\n//     literal, so `[label\\]: url` is NOT treated as an LRD (renders as\n//     literal text).\n//   - URL scheme abuse: javascript:, data:text/html, vbscript:, blob:,\n//     protocol-relative //, mailto: with prefill phishing parameters.\n//     Allowlist-only (URL / ImageURL).\n//   - HTML attribute / element breakout: `\"`, `\u003c`, `\u003e`, `\u0026`, `'` inside\n//     HTML lexical slots. Handled by HTMLEscape.\n//   - CommonMark §2.3 NUL: replaced with U+FFFD by Block, InlineText,\n//     LinkTitle, TableCell, HTMLEscape, InlineCode, CodeBlock, and\n//     LanguageCodeBlock.\n//   - code-fence leakage: a user-opened ``` ``` ``` fence that runs to EOF\n//     with no closing fence, which would otherwise swallow every realm-\n//     emitted line that follows. Block auto-closes any open fence at EOF.\n//   - table-alignment drift: tabs inside table cells expanding to variable\n//     widths (1-4 spaces depending on column position) and shifting cell\n//     boundaries unpredictably. TableCell replaces tabs with single spaces.\n//\n// What this package does NOT do:\n//\n//   - It does not store state. Every helper is a pure function.\n//   - It does not validate semantic correctness. sanitize.URL accepts\n//     a syntactically valid https:// URL even if the host is malicious;\n//     URL reputation is a separate layer.\n//   - It does not enforce CSS containment. ImageURL admits data:image/*\n//     URIs on the assumption that the deploying gnoweb instance caps\n//     rendered image dimensions via CSS. Without that cap, a malicious\n//     image can blow out the page layout or exhaust memory.\n//   - It does not perform structural sandboxing of foreign markdown.\n//     If a realm concatenates an opaque markdown blob returned from a\n//     polymorphic interface (`someThing.Render()`), it needs a structural\n//     sandbox primitive (e.g. a `\u003cgno-card\u003e` extension), not just leaf\n//     sanitization.\n//\n// # When to use Block vs BlockRich\n//\n// Both are safe sanitizers; both run identical realm-binding defenses.\n// They differ in what user-authored block structure survives:\n//\n//   - Block — paragraph-shaped only. Escapes `#`, `\u003e`, list markers,\n//     `---`/`***`/`___` thematic breaks, and `===`/`---` setext\n//     underlines. Use for leaf slots — footnote definition bodies,\n//     table cells, blockquote bodies (Blockquote uses Block), single-\n//     paragraph prose, any slot where richer structure has no benefit\n//     or where richer structure could visually impersonate realm chrome.\n//\n//   - BlockRich — full-richness. Preserves user-authored headings,\n//     lists, quotes, HR, setext. Use for user content the realm intends\n//     to compose with full block-level structure, typically inside a\n//     sandbox container (`\u003cgno-card\u003e`, `\u003cgno-foreign\u003e`) or a CSS-demoted\n//     region. BlockRich's qualifying-setext defense prevents the\n//     cross-boundary attack (user content reaching back to promote\n//     realm chrome to a heading), but inner-heading visual containment\n//     is the realm's CSS responsibility. gnoweb does not yet ship CSS\n//     rules that demote headings inside sandbox containers — until they\n//     land, BlockRich + sandbox renders inner headings at literal size.\n//\n// Do NOT compose Block and BlockRich in either direction. Pick one\n// helper at the right level.\n//\n// # Extending\n//\n// A new helper added to this package MUST:\n//\n//  1. Be panic-free for any string input.\n//  2. Strip bidi+zero-width before any other transform (so display\n//     equals storage end-to-end).\n//  3. Declare its idempotence class in the table above.\n//  4. Document the markdown / HTML lexical slot it targets.\n//  5. Reject rather than partially-sanitize when input is structurally\n//     invalid (return \"\" — never half-process an address or URL).\n//  6. Pick exactly one of the two return-value contracts and stick to\n//     it: escapers always return a transformed string and never reject\n//     (any input is OK — the transformation makes it safe); validators\n//     return the cleaned input verbatim on accept or \"\" on reject and\n//     never half-process. Mixing the contracts within one helper is a\n//     bug — callers can't reason about whether \"\" means \"input was\n//     already empty\" or \"input was rejected\".\npackage sanitize\n\nimport (\n\t\"chain/markdown\"\n\t\"html\"\n\t\"strings\"\n)\n\n// ----- Re-exports of the public chain/markdown natives -----\n//\n// These are general-purpose data-hygiene primitives, not markdown-specific.\n// The other helpers in this package call them internally, so realms emitting\n// markdown rarely need to call them directly. Reach for these when you have\n// a non-markdown use case — e.g. normalizing a username before storage,\n// canonicalizing a search query, or stripping invisible characters from\n// any user string that will be displayed or compared.\n\n// StripBidiAndZeroWidth removes Unicode bidi controls and zero-width\n// characters (U+200B-D, U+200E-F, U+202A-E, U+2066-9, U+FEFF) from s.\n// Use it when storing or comparing user-supplied strings outside of a\n// markdown context — for example, before saving a display name to state,\n// or before hashing a search query. Idempotent: calling twice gives the\n// same result.\n//\n// Thin wrapper over chain/markdown.StripBidiAndZeroWidth.\nfunc StripBidiAndZeroWidth(s string) string {\n\treturn markdown.StripBidiAndZeroWidth(s)\n}\n\n// NormalizeBreaks unifies CR-LF and lone CR to LF (CommonMark §2.2 line\n// endings only — does NOT touch U+2028/U+2029). Use it when comparing\n// or hashing user input that may have been authored on different\n// platforms (Windows CRLF vs. Unix LF), so equivalent strings normalize\n// to the same bytes. Idempotent.\n//\n// Thin wrapper over chain/markdown.NormalizeBreaks.\nfunc NormalizeBreaks(s string) string {\n\treturn markdown.NormalizeBreaks(s)\n}\n\n// ----- Escapers -----\n\n// InlineText prepares an arbitrary user string for an INLINE markdown\n// slot — anywhere the rendered output stays on a single line and lives\n// inside a larger markdown construct.\n//\n// Use for:\n//   - link text:        [InlineText(label)](url)\n//   - heading text:     # InlineText(title)\n//   - bold/italic body: **InlineText(name)**\n//   - image alt text:   ![InlineText(alt)](src)\n//   - single-line block-context slots:\n//     \u003e [!NOTE] InlineText(title)\n//     \u003e Author: InlineText(name)\n//\n// Multi-paragraph prose belongs in Block, not InlineText. InlineText\n// folds every newline to a single space (so paragraph structure is\n// erased) and escapes inline-active CommonMark punctuation:\n//\n//\t\\ * _ [ ] ( ) ~ \u003e - + . ! ` # \u003c \u0026\n//\n// Two characters are intentionally NOT escaped:\n//\n//   - `|` — only meaningful in GFM table rows. Leaving it literal here\n//     lets TableCell (which calls InlineText then escapes `|` itself)\n//     avoid double-escaping pipes into `\\\\|`.\n//   - `=` — only meaningful as a setext heading underline, which is a\n//     line-level construct. Escaping `=` inline would mangle expressions\n//     like `x = 1` for no benefit.\n//\n// Not idempotent (see package doc).\nfunc InlineText(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\ts = markdown.NormalizeBreaks(s)       // CM §2.2 \\r\\n / \\r → \\n\n\ts = foldNewlinesAndSeparators(s, ' ') // \\n + NEL + U+2028/U+2029 → space\n\treturn markdown.EscapeInline(s)\n}\n\n// Block prepares user content for a top-level BLOCK markdown context\n// where paragraphs, line breaks, code blocks, and other block structure\n// should survive — but where the content must NOT be able to inject\n// new top-level constructs (headings, lists, blockquotes,\n// link-reference definitions, setext underlines, gnoweb extension\n// delimiters, GFM table rows).\n//\n// Output shape: every non-empty result begins AND ends with \"\\n\\n\" —\n// CM §4.8 blank lines on both sides — so user content is guaranteed\n// to occupy its own paragraph(s), isolated from any realm chrome that\n// precedes OR follows it. This bounds CM §4.6 HTML block types 6 and\n// 7 (`\u003cdiv\u003e`, `\u003ctable\u003e`, `\u003cform\u003e`, arbitrary `\u003cfoo\u003e` tags) which\n// close on a blank line and are NOT escaped in any mode, and it\n// defeats first-line setext promotion (`===`/`---`) that strict-mode\n// escapes miss when the previous line is blank in the user input but\n// non-blank in the concatenated realm output. Empty input (or input\n// that strips entirely, e.g. a lone LRD) returns \"\" — no envelope is\n// emitted.\n//\n// Use for any multi-paragraph user-supplied prose that the realm\n// concatenates into its rendered output:\n//   - post bodies, comments, replies\n//   - profile bios, About sections\n//   - proposal descriptions, governance motions\n//   - changelog entries, release notes\n//\n// What Block does with each kind of attacker input:\n//\n//\tUser attempt                                          | Block's response\n//\t------------------------------------------------------|----------------------------------------------------\n//\t  --- preserved verbatim ---                          |\n//\t[text](url) inline link, ![alt](src) image            | preserved verbatim\n//\t------------------------------------------------------|----------------------------------------------------\n//\t  --- escaped / stripped / folded ---                 |\n//\t# heading at line-start                               | escaped → literal `# heading`\n//\t\u003e quoted at line-start                                | escaped → literal `\u003e`\n//\t- item, * item, + item, 1. item at line-start         | escaped\n//\t---, ***, ___ (3+) at line-start                      | escaped\n//\t=== or --- on its own line after non-blank text       | escaped (no setext promotion of the line above)\n//\t\u003cgno-card\u003e, \u003cgno-columns\u003e, any \u003cgno-…\u003e/\u003c/gno-…\u003e at    | escaped (wildcard match) → literal text\n//\t  line-start                                          |\n//\t| a | b | GFM table row (line-leading `|`)            | escaped → literal `| a | b |`\n//\t\u003c!--, \u003cscript\u003e, \u003cpre\u003e, \u003cstyle\u003e, \u003ctextarea\u003e, \u003c?…?\u003e,    | escaped (\\\u003c…) → literal text;\n//\t  \u003c!DOCTYPE…\u003e, \u003c![CDATA[…]]\u003e at line-start            |   blocks goldmark from opening a\n//\t  (CM §4.6 HTML block types 1-5)                      |   blank-line-NON-terminating HTML block\n//\t[text][realm-label] ref-link USE                      | both bracket pairs escaped → \\[text\\]\\[realm-label\\]\n//\t[^name] footnote-ref                                  | both brackets escaped → \\[^name\\]\n//\t[label] bare shortcut-ref                             | both brackets escaped → \\[label\\]\n//\t[label]: url link-reference definition                | whole region stripped (incl. multi-line label\n//\t  (incl. [lab\\nel]: url multi-line)                   |   `[lab\\nel]: url` and any title continuation)\n//\t[label\\]: url (backslash-escaped `]`)                 | NOT stripped; brackets escaped → paragraph text\n//\tcode fence opened without close                       | autoclosed at end of input\n//\tNUL byte (\\x00)                                       | replaced with U+FFFD\n//\tU+2028 / U+2029 / U+0085 (NEL)                        | folded to `\\n`\n//\tbidi/zero-width controls                              | stripped\n//\n// COMPOSITION GOTCHA: Block's EOF fence-autoclose appends a final\n// fence line. If you wrap Block's output with a line-prefixing\n// builder like md.Blockquote (which prepends `\u003e ` per line) or\n// md.Nested, that closing fence becomes a prefixed line. The output\n// is still safe (the fence still closes correctly) but may render\n// awkwardly. If pixel-perfect output matters, strip a trailing blank\n// fence line after Block.\n//\n// Why backslash and not a space for `\u003cgno-…\u003e` lines: gnoweb's\n// extension parsers call `util.TrimLeftSpace` on the line before tag\n// matching, which would strip a leading space and let the tag match\n// anyway. A leading `\\` survives the trim (only ASCII whitespace +\n// form-feed are stripped) and is consumed by the inline escape phase\n// before Type-7 HTML block detection can fire (Type-7 requires the\n// first non-whitespace char to be `\u003c`).\n//\n// Inline emphasis, code spans, inline links, and soft line breaks\n// within a paragraph are PRESERVED — users can format. Pipes that\n// are NOT at line-start stay literal so prose can still write things\n// like `a | b`.\n//\n// Idempotent: Block(Block(s)) is byte-identical to Block(s). The\n// bracket walker strips LRDs on the first pass; remaining `[`/`]`\n// outside inline-link/image spans are escaped to `\\[`/`\\]`, and\n// already-escaped brackets are preserved on subsequent passes\n// (pass-2 backslash-parity tracking). Still, wrap each user-supplied\n// string exactly once — chained sanitization adds no value and\n// burns gas.\nfunc Block(s string) string {\n\ts = markdown.NormalizeBreaks(s)\n\ts = markdown.StripBidiAndZeroWidth(s)\n\ts = replaceNULWithFFFD(s)\n\ts = markdown.EscapeBlockHazards(s)\n\t// Symmetric \"\\n\\n\" envelope — same pattern BlockRich uses for the\n\t// same reasons (see BlockRich docstring \"Cross-paragraph safety\").\n\t// Strict mode escapes most line-leading hazards (setext, GFM table\n\t// row, CM §4.6 HTML types 1-5, list/heading/HR markers), but two\n\t// hazards remain that only a blank-line break can close:\n\t//\n\t//   - CM §4.6 HTML block types 6 and 7 (`\u003cdiv\u003e`, `\u003ctable\u003e`,\n\t//     `\u003cform\u003e`, arbitrary `\u003cfoo\u003e` tags) are NOT escaped in any mode\n\t//     — they close on a blank line per CM. Without a trailing\n\t//     \"\\n\\n\", a `\u003cdiv\u003e` at the end of user content extends into\n\t//     appended realm chrome.\n\t//\n\t//   - First-line setext: strict mode's setext escape only fires\n\t//     when the previous line is non-blank IN THE USER'S INPUT.\n\t//     A user whose first line is `===` slips past, and concatenated\n\t//     after `chrome\\n` would promote chrome to H1. The leading\n\t//     \"\\n\\n\" forces a paragraph break so chrome cannot be merged.\n\t//\n\t// TrimLeft/TrimRight + fixed wrap is idempotent: Block(Block(s)) is\n\t// byte-identical to Block(s). Empty post-escape result short-\n\t// circuits to \"\" so realm concatenation doesn't leak stray blank\n\t// lines for trivially empty inputs (e.g. lone LRD that strips\n\t// entirely).\n\ts = strings.TrimLeft(s, \"\\n\")\n\ts = strings.TrimRight(s, \"\\n\")\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\treturn \"\\n\\n\" + s + \"\\n\\n\"\n}\n\n// BlockRich is the permissive counterpart of Block. Both are safe\n// sanitizers — the distinction is what markdown structure survives:\n//\n//   - Block escapes line-leading block markers (`#`, `\u003e`, `-`, `*`,\n//     `+`, `1.`), thematic breaks (`---`/`***`/`___`), and setext\n//     underlines (`===`/`---`). User content becomes paragraph-shaped.\n//   - BlockRich PRESERVES all of those, so user content can compose\n//     headings, lists, quotes, horizontal rules, and setext-styled\n//     headings. Realm-binding defenses stay on (extension delimiters\n//     `\u003cgno-…\u003e`, the bracket walker for link / LRD / ref /\n//     footnote / shortcut, fence autoclose, NUL / bidi /\n//     Unicode-separator folding). GFM table-row openers are\n//     PRESERVED (see \"Tables\" below).\n//\n// Cross-paragraph safety: BlockRich's output begins with \"\\n\\n\"\n// AND ends with \"\\n\\n\" — CM §4.8 blank lines on both sides — so\n// user content is guaranteed to occupy its own paragraph(s),\n// isolated from anything the realm emits before OR after. Symmetric\n// isolation closes four distinct attacks:\n//\n//   - Cross-paragraph setext promotion (backward). User content\n//     `body\\n===\\nmore` concatenated after realm chrome (no\n//     trailing `\\n`) would, without paragraph isolation, place\n//     \"chrome\\nbody\" in one paragraph; the `===` setext underline\n//     would then promote that merged paragraph to H1, hijacking\n//     realm chrome. The leading \"\\n\\n\" forces a paragraph break.\n//\n//   - Cross-paragraph GFM table promotion (backward). User content\n//     beginning with `|---|---|` (a table delimiter row) would,\n//     without a blank-line break, retroactively turn the preceding\n//     realm line into a `\u003cthead\u003e`. Paragraph isolation prevents\n//     the table-detection scan from crossing the boundary.\n//\n//   - Cross-paragraph GFM table promotion (forward). Realm chrome\n//     appended immediately after BlockRich(user) that begins with\n//     `|---|` would, without a trailing blank line, extend user's\n//     last line into a table header and pull realm chrome into the\n//     body row. The trailing \"\\n\\n\" prevents the merge.\n//\n//   - Lazy paragraph continuation (forward). Paragraph-shaped realm\n//     chrome appended immediately after BlockRich(user) would, via\n//     CM §5.2, merge into user's trailing paragraph and inherit any\n//     block-level decoration it carries.\n//\n// First-line qualifying-setext escape (the\n// `neuterLeadingSetextIfQualifying` pre-pass) remains in place as\n// belt-and-suspenders: if the first non-blank line of user input\n// matches the CM §4.3 setext-underline pattern (run of `=` or `-`\n// with 0-3 leading spaces and only trailing whitespace), BlockRich\n// inserts `\\` before the first `=`/`-`. This is redundant given\n// paragraph isolation but harmless and inexpensive.\n//\n// # Tables\n//\n// BlockRich preserves line-leading `|` so user content can\n// compose GFM tables:\n//\n//\t| Header A | Header B |\n//\t|----------|----------|\n//\t| cell a   | cell b   |\n//\n// renders as a real `\u003ctable\u003e` element. Strict Block continues to\n// escape line-leading `|` (each row becomes literal `\\| a | b |`\n// text). When the realm authors the table itself and inserts user\n// content into a specific cell, use TableCell — NOT BlockRich —\n// to sanitize that cell value.\n//\n// What attacker input produces what (full table, same rows as Block\n// except where marked CHANGED):\n//\n//\tUser attempt                                  | BlockRich response\n//\t----------------------------------------------|--------------------------------------------------\n//\t  --- preserved (compose freely) ---          |\n//\t# heading at line-start                       | preserved [CHANGED from Block]\n//\t\u003e quoted at line-start                        | preserved [CHANGED]\n//\t- item, * item, + item, 1. item               | preserved [CHANGED]\n//\t---, ***, ___ thematic break                  | preserved [CHANGED]\n//\t=== or --- setext underline                   | preserved when preceded by user text;\n//\t                                              | escaped (\\===/\\---) if the first non-blank\n//\t                                              | line of input [CHANGED]\n//\t| a | b | GFM table row (line-leading |)      | preserved → renders as \u003ctable\u003e when followed by\n//\t                                              | a delimiter row [CHANGED]\n//\t[text](url), ![alt](src)                      | preserved verbatim [SAME]\n//\t----------------------------------------------|--------------------------------------------------\n//\t  --- escaped / stripped / folded ---         |\n//\t\u003cgno-card\u003e, any \u003cgno-…\u003e/\u003c/gno-…\u003e at line-start| escaped (wildcard match) [SAME]\n//\t\u003c!--, \u003cscript\u003e, \u003cpre\u003e, \u003cstyle\u003e, \u003ctextarea\u003e,   | escaped (\\\u003c…) [SAME] — Types 1-5 don't close\n//\t  \u003c?…?\u003e, \u003c!DOCTYPE…\u003e, \u003c![CDATA[…]]\u003e           |   on blank lines, so `\\n\\n` envelope\n//\t  at line-start (CM §4.6 HTML block types 1-5)|   doesn't isolate them; explicit escape\n//\t[text][realm-label] ref-link USE              | both pairs escaped [SAME]\n//\t[^name] footnote-ref                          | both brackets escaped [SAME]\n//\t[label] bare shortcut-ref                     | both brackets escaped [SAME]\n//\t[label]: url link-reference definition        | whole region stripped [SAME]\n//\t[label\\]: url (escaped `]`)                   | not stripped; brackets escaped [SAME]\n//\tcode fence opened without close               | autoclosed at end of input [SAME]\n//\tNUL byte (\\x00)                               | replaced with U+FFFD [SAME]\n//\tU+2028 / U+2029 / U+0085 (NEL)                | folded to `\\n` [SAME]\n//\tbidi/zero-width controls                      | stripped [SAME]\n//\n// Use BlockRich for user content the realm intends to compose with\n// full block-level richness — typically inside a sandbox container\n// (`\u003cgno-card\u003e`, `\u003cgno-foreign\u003e`) or a CSS-demoted region where inner\n// headings render visually distinct from realm chrome. The realm\n// must own the visual containment: concatenating BlockRich's output\n// directly into a top-level page still lets the user write `# heading`\n// at document level. BlockRich's cross-boundary setext defense prevents\n// the worst case (reaching backwards into realm bytes), but visual\n// containment of inner headings is the realm's CSS responsibility.\n// gnoweb does not yet ship CSS rules that demote inner headings inside\n// `\u003cgno-card\u003e` / `\u003cgno-foreign\u003e` — until those rules land, realms\n// using BlockRich + a sandbox should be aware that inner headings\n// render at their literal level.\n//\n// Idempotent: BlockRich(BlockRich(s)) is byte-identical to\n// BlockRich(s). The TrimLeft-then-\"\\n\\n\"-prepend pattern strips\n// any leading newlines and reapplies exactly two, so the leading\n// shape is stable across passes; the qualifying-setext escape is\n// stable (a line beginning with `\\` no longer matches the setext\n// pattern); and the bracket walker treats already-escaped\n// `\\[`/`\\]` as ordinary bytes. Empty input (or input that strips\n// to empty, e.g. a lone link-reference definition) returns \"\" —\n// realm concatenation doesn't get a stray blank line.\n// Still, wrap each user-supplied string exactly once — chained\n// sanitization adds no value and burns gas.\n//\n// Realm-discipline boundary: BlockRich defends user input against\n// every cross-paragraph attack listed above, but it CANNOT defend\n// against malformed REALM chrome. Specifically, callers should emit\n// realm chrome at flush-left column 0 around `BlockRich(user)`. If\n// the realm chrome BEFORE the call contains an unclosed CM §4.6\n// Type 1 HTML tag (`\u003cscript\u003e`, `\u003cpre\u003e`, `\u003cstyle\u003e`, `\u003ctextarea\u003e`),\n// the `\\n\\n` envelope does NOT close it (Type 1 closes only on the\n// matching close tag), and user-controlled `\u003c/tag\u003e` content can\n// then prematurely terminate it. Indented chrome (4+ leading\n// spaces, list-item continuations, footnote-definition body) can\n// likewise extend across the envelope into user content. Keep\n// chrome flush-left and Type 1 tags closed within the chrome.\n//\n// PREVIEW: BlockquoteRich is currently the only in-tree caller of\n// BlockRich; the API and the `\"\\n\\n\"` output shape may evolve once\n// direct callers emerge.\nfunc BlockRich(s string) string {\n\ts = markdown.NormalizeBreaks(s)\n\ts = markdown.StripBidiAndZeroWidth(s)\n\ts = replaceNULWithFFFD(s)\n\t// Fold Unicode separators (U+2028, U+2029, U+0085 NEL) to '\\n'\n\t// BEFORE the setext-qualifying check. The native\n\t// EscapeBlockHazardsRich also folds them internally, but the Gno\n\t// helper below needs to see the folded form to correctly identify\n\t// the first non-blank line — otherwise an attacker can hide the\n\t// `===` setext underline behind a U+2028 / U+2029 / U+0085 and\n\t// reach back to promote realm chrome above it.\n\ts = foldSeparatorsToNewline(s)\n\ts = neuterLeadingSetextIfQualifying(s)\n\ts = markdown.EscapeBlockHazardsRich(s)\n\t// Ensure the output BOTH starts AND ends with \"\\n\\n\" — CM §4.8\n\t// blank lines on each side — so user content is GUARANTEED to\n\t// occupy its own paragraph(s), isolated from anything the realm\n\t// emits before OR after. Symmetric isolation closes four attacks:\n\t//\n\t//  Backward (closed by leading \"\\n\\n\"):\n\t//   1. Deeper-setext: user content `body\\n===\\nmore` concatenated\n\t//      after realm chrome (no trailing `\\n`) would otherwise place\n\t//      \"chrome\\nbody\" in one paragraph; the `===` setext underline\n\t//      would then promote that merged paragraph to H1, hijacking\n\t//      realm chrome.\n\t//   2. GFM table-row promotion: user content beginning with\n\t//      `|---|---|` (a table delimiter row) would, without a blank-\n\t//      line break, retroactively promote the preceding realm line\n\t//      into a `\u003cthead\u003e` cell.\n\t//\n\t//  Forward (closed by trailing \"\\n\\n\"):\n\t//   3. GFM table-row promotion in reverse: realm appending its own\n\t//      chrome immediately after BlockRich(user), where chrome\n\t//      starts with `|---|`, would extend user's last line into a\n\t//      table header and pull realm chrome into the body row.\n\t//   4. Lazy paragraph continuation: realm appending paragraph-\n\t//      shaped chrome immediately after BlockRich(user) would, via\n\t//      CM §5.2 lazy-continuation, merge into user's trailing\n\t//      paragraph and inherit any block-level decoration it carries.\n\t//\n\t// `neuterLeadingSetextIfQualifying` above is now belt-and-\n\t// suspenders for the first-line setext case: even if the blank-\n\t// line guarantee were somehow defeated by an exotic CM consumer,\n\t// the first-line escape still blocks the simplest setext shape.\n\t//\n\t// Empty post-escape result short-circuits to \"\" so realm\n\t// concatenation doesn't leak stray blank lines for trivially empty\n\t// inputs (e.g. a lone link-reference definition that strips\n\t// entirely).\n\t//\n\t// Idempotency: TrimLeft and TrimRight strip ALL leading/trailing\n\t// \"\\n\"s, then the wrap adds exactly two on each side. Stable\n\t// across passes.\n\ts = strings.TrimLeft(s, \"\\n\")\n\ts = strings.TrimRight(s, \"\\n\")\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\treturn \"\\n\\n\" + s + \"\\n\\n\"\n}\n\n// foldSeparatorsToNewline replaces U+0085 NEL (0xC2 0x85),\n// U+2028 (0xE2 0x80 0xA8), and U+2029 (0xE2 0x80 0xA9) with '\\n'.\n// Leaves '\\n' bytes alone. Used by BlockRich so the qualifying-setext\n// pre-pass and the native both see the same line structure.\nfunc foldSeparatorsToNewline(s string) string {\n\t// Cheap pre-check: only the 0xC2 / 0xE2 lead bytes can trigger.\n\tif !containsAnyByteForFold(s) {\n\t\treturn s\n\t}\n\tout := make([]byte, 0, len(s))\n\tfor i := 0; i \u003c len(s); {\n\t\tc := s[i]\n\t\tif c == 0xC2 \u0026\u0026 i+1 \u003c len(s) \u0026\u0026 s[i+1] == 0x85 {\n\t\t\tout = append(out, '\\n')\n\t\t\ti += 2\n\t\t\tcontinue\n\t\t}\n\t\tif c == 0xE2 \u0026\u0026 i+2 \u003c len(s) \u0026\u0026 s[i+1] == 0x80 \u0026\u0026 (s[i+2] == 0xA8 || s[i+2] == 0xA9) {\n\t\t\tout = append(out, '\\n')\n\t\t\ti += 3\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, c)\n\t\ti++\n\t}\n\treturn string(out)\n}\n\nfunc containsAnyByteForFold(s string) bool {\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tif s[i] == 0xC2 || s[i] == 0xE2 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// neuterLeadingSetextIfQualifying scans s for the first non-blank\n// line. If that line matches the CommonMark §4.3 setext-underline\n// pattern (0-3 leading spaces, then a run of all `=` or all `-`,\n// then optional trailing whitespace, then `\\n` or EOF), the function\n// returns s with a `\\` inserted before the first `=`/`-`. Otherwise\n// returns s unchanged. The escape prevents a realm-emitted line above\n// BlockRich's output from being retroactively promoted to a heading.\nfunc neuterLeadingSetextIfQualifying(s string) string {\n\tpos := 0\n\tfor pos \u003c len(s) {\n\t\t// Walk to the first non-whitespace byte of the current line.\n\t\tlineStart := pos\n\t\ti := pos\n\t\tfor i \u003c len(s) \u0026\u0026 (s[i] == ' ' || s[i] == '\\t') {\n\t\t\ti++\n\t\t}\n\t\tif i \u003e= len(s) || s[i] == '\\n' {\n\t\t\t// Blank line; advance to next line.\n\t\t\tif i \u003e= len(s) {\n\t\t\t\treturn s\n\t\t\t}\n\t\t\tpos = i + 1\n\t\t\tcontinue\n\t\t}\n\t\t// First non-blank line. Check setext-underline shape.\n\t\tif i-lineStart \u003e 3 {\n\t\t\treturn s // 4+ leading spaces = indented code, not setext\n\t\t}\n\t\tc := s[i]\n\t\tif c != '=' \u0026\u0026 c != '-' {\n\t\t\treturn s // not a setext underline candidate\n\t\t}\n\t\tj := i + 1\n\t\tfor j \u003c len(s) \u0026\u0026 s[j] == c {\n\t\t\tj++\n\t\t}\n\t\tfor j \u003c len(s) \u0026\u0026 (s[j] == ' ' || s[j] == '\\t') {\n\t\t\tj++\n\t\t}\n\t\tif j \u003c len(s) \u0026\u0026 s[j] != '\\n' {\n\t\t\treturn s // mixed content on the line — not setext\n\t\t}\n\t\treturn s[:i] + \"\\\\\" + s[i:]\n\t}\n\treturn s\n}\n\n// Blockquote wraps user content as a CommonMark blockquote: each line\n// of the cleaned content gets a \"\u003e \" prefix so the renderer displays\n// it inside a `\u003cblockquote\u003e` element.\n//\n// Use for any multi-paragraph user-supplied text that the realm wants\n// to render as a quotation: cited posts, attached responses, error\n// snapshots that should visually stand out.\n//\n// The content is first cleaned by Block (bidi-strip, line-ending\n// normalize, NUL→U+FFFD, bracket walker for link/image/LRD spans,\n// block-marker escape, code-fence auto-close at EOF, Unicode-separator\n// fold). Block's \"\\n\\n\" cross-paragraph envelope is then stripped —\n// the `\u003e ` marker creates the container boundary, so the envelope\n// would only line-prefix to empty `\u003e ` lines top and bottom — and\n// every remaining line is prefixed with \"\u003e \". The user content can\n// still use inline emphasis, code spans, and nested fenced code blocks\n// inside the quote; what it cannot do is open new top-level structure\n// (heading, list, blockquote, GFM table row, etc.) or escape the\n// quote.\n//\n// Output shape — every non-empty result begins with \"\\n\" and ends\n// with \"\\n\\n\" (same shape as BlockquoteRich):\n//\n//   - Leading \"\\n\" guarantees a clean blockquote opener even when the\n//     realm concatenates `chrome + Blockquote(user)` without its own\n//     newline separator.\n//   - Trailing \"\\n\\n\" (blank line) cleanly ends the blockquote so a\n//     realm appending `Blockquote(user) + chrome` cannot pull chrome\n//     bytes into the quote via CommonMark §5.2 lazy continuation.\n//\n// Empty input (or input that strips entirely, e.g. a lone LRD)\n// returns \"\" — no blockquote is emitted.\n//\n// Composition gotcha: Block's EOF code-fence auto-close (added when\n// user content opens a ``` ``` ``` fence without closing it) becomes a\n// \"\u003e ```\" line at the end of the blockquote. Goldmark parses this\n// correctly as the close of a fenced block inside the quote — the\n// output is structurally safe — but the markdown source looks unusual\n// to a human reviewer. If aesthetic output matters, ensure user\n// content closes its own fences.\n//\n// Not idempotent (see package doc): wraps with `\u003e ` per line, so\n// calling twice double-wraps and the outer call's Block step escapes\n// the inner `\u003e` prefixes.\n//\n// Do NOT compose with BlockRich in either direction:\n//   - Blockquote(BlockRich(s)) double-sanitizes: BlockRich preserves\n//     `#`/`\u003e`/etc., then Blockquote's Block step escapes them again.\n//   - BlockRich(Blockquote(s)) doesn't make sense: Blockquote already\n//     line-prefixed with `\u003e `; BlockRich expects raw user content.\n//\n// For a quoted body that can contain headings, lists, nested quotes,\n// or thematic breaks, use BlockquoteRich.\nfunc Blockquote(text string) string {\n\ttext = Block(text)\n\t// Block wraps its output with \"\\n\\n\" on each side for cross-\n\t// paragraph isolation. Inside a blockquote both wraps are redundant\n\t// — the `\u003e ` marker creates the container boundary — and they\n\t// would line-prefix to two useless `\u003e ` empty quoted lines top and\n\t// bottom. Strip ALL leading and trailing \"\\n\"s so the body starts\n\t// and ends clean; this helper re-wraps with `\\n` + body + `\\n\\n`\n\t// below (same shape as BlockquoteRich).\n\ttext = strings.TrimLeft(text, \"\\n\")\n\tif text == \"\" {\n\t\treturn \"\"\n\t}\n\ttext = strings.TrimRight(text, \"\\n\")\n\tif text == \"\" {\n\t\treturn \"\"\n\t}\n\tvar sb strings.Builder\n\tsb.WriteByte('\\n')\n\tfor _, line := range strings.Split(text, \"\\n\") {\n\t\tsb.WriteString(\"\u003e \")\n\t\tsb.WriteString(line)\n\t\tsb.WriteByte('\\n')\n\t}\n\tsb.WriteByte('\\n')\n\treturn sb.String()\n}\n\n// BlockquoteRich is the permissive counterpart of Blockquote. Both\n// wrap user content as a CommonMark blockquote (each line prefixed\n// with `\u003e `), but they differ in what block-level structure inside\n// the quote survives:\n//\n//   - Blockquote escapes line-leading block markers, so the quoted\n//     body is paragraph-shaped — `# x` inside a Blockquote stays a\n//     literal `#`.\n//   - BlockquoteRich PRESERVES line-leading block markers, so the\n//     quoted body can compose ATX headings, lists, thematic breaks,\n//     nested blockquotes (`\u003e \u003e nested`), and other block-level\n//     structure. Realm-binding defenses stay on (extension delimiters,\n//     GFM table-row openers, bracket walker, fence autoclose,\n//     NUL / bidi / Unicode-separator folding).\n//\n// Output shape — every non-empty result begins with \"\\n\" and ends\n// with \"\\n\\n\":\n//\n//   - Leading \"\\n\" guarantees a clean blockquote opener even when the\n//     realm concatenates `chrome + BlockquoteRich(user)` without its\n//     own newline separator. Without the leading \"\\n\", chrome ending\n//     mid-line followed by \"\u003e quoted\" would render `\u003e` as literal\n//     paragraph text instead of opening a blockquote.\n//   - Trailing \"\\n\\n\" (blank line) cleanly ends the blockquote so a\n//     realm appending `BlockquoteRich(user) + chrome` cannot pull\n//     chrome bytes into the quote via CommonMark §5.2 lazy\n//     continuation. Without the trailing blank line, paragraph chrome\n//     immediately after BlockquoteRich would render inside the quote.\n//   - BlockRich's own leading \"\\n\\n\" (paragraph-isolation blank line)\n//     is stripped before line-prefixing — otherwise the output would\n//     carry one or two redundant empty `\u003e ` quoted lines at the top.\n//     A single \"\\n\" is then re-prepended at the BlockquoteRich\n//     boundary so `chrome + BlockquoteRich(user)` still lands the\n//     first `\u003e` at column 0.\n//   - The cross-boundary setext defense BlockRich provides is\n//     redundant inside a blockquote: a setext underline inside `\u003e `\n//     content can only promote a line in the same blockquote, never\n//     reach realm bytes (different CM container). BlockRich still\n//     applies it, harmlessly.\n//\n// What attacker input produces what (rows that differ from\n// Blockquote are marked CHANGED):\n//\n//\tUser attempt                                  | BlockquoteRich response\n//\t----------------------------------------------|------------------------------------------------\n//\t  --- preserved inside `\u003e ` quote ---         |\n//\t# heading                                     | preserved as `\u003e # heading` [CHANGED]\n//\t\u003e nested quote                                | preserved as `\u003e \u003e nested quote` [CHANGED]\n//\t- item, * item, + item, 1. item               | preserved as `\u003e - item` etc. [CHANGED]\n//\t---, ***, ___ thematic break                  | preserved [CHANGED]\n//\t=== or --- setext underline                   | preserved when preceded by user text;\n//\t                                              | escaped (\\===/\\---) if first non-blank\n//\t                                              | line of input [CHANGED]\n//\t| a | b | GFM table row (line-leading |)      | preserved → renders as \u003ctable\u003e inside the\n//\t                                              | blockquote when followed by a delimiter row [CHANGED]\n//\t[text](url), ![alt](src)                      | preserved verbatim [SAME]\n//\t----------------------------------------------|------------------------------------------------\n//\t  --- escaped / stripped / folded ---         |\n//\t\u003cgno-card\u003e, any \u003cgno-…\u003e/\u003c/gno-…\u003e at line-start| escaped (wildcard match) [SAME]\n//\t\u003c!--, \u003cscript\u003e, \u003cpre\u003e, \u003cstyle\u003e, \u003ctextarea\u003e,   | escaped (\\\u003c…) [SAME] — CM §4.6 Types 1-5\n//\t  \u003c?…?\u003e, \u003c!DOCTYPE…\u003e, \u003c![CDATA[…]]\u003e           |   don't close on blank lines; without escape\n//\t  at line-start                               |   they would swallow chrome past the `\u003e ` quote\n//\t[text][realm-label] ref-link USE              | both pairs escaped [SAME]\n//\t[^name] footnote-ref                          | both brackets escaped [SAME]\n//\t[label] bare shortcut-ref                     | both brackets escaped [SAME]\n//\t[label]: url link-reference definition        | whole region stripped [SAME]\n//\tcode fence opened without close               | autoclosed at end of input [SAME]\n//\tNUL byte (\\x00)                               | replaced with U+FFFD [SAME]\n//\tU+2028 / U+2029 / U+0085 (NEL)                | folded to `\\n` [SAME]\n//\tbidi/zero-width controls                      | stripped [SAME]\n//\n// Use BlockquoteRich when the realm wants to render user content as\n// a quotation that itself reads like authored markdown — the visual\n// CSS containment of `\u003cblockquote\u003e` already demotes inner headings\n// relative to realm chrome, so the \"inner headings need a sandbox\"\n// caveat that applies to BlockRich at top level does not apply here.\n//\n// Not idempotent: like Blockquote, calling twice double-wraps —\n// `BlockquoteRich(BlockquoteRich(s))` produces `\u003e \u003e content`,\n// nesting the quote a level deeper each pass.\n//\n// Empty input (or input that reduces to nothing after BlockRich,\n// e.g. a lone link-reference definition) returns \"\" — no blockquote\n// is emitted and neither the leading \"\\n\" nor the trailing \"\\n\\n\"\n// shape applies.\nfunc BlockquoteRich(text string) string {\n\ttext = BlockRich(text)\n\t// BlockRich wraps user content with \"\\n\\n\" on each side for\n\t// cross-paragraph isolation. Inside a blockquote both wraps are\n\t// redundant — the `\u003e ` marker creates the container boundary —\n\t// and they would line-prefix to two useless `\u003e ` empty quoted\n\t// lines top and bottom. Strip ALL leading and trailing \"\\n\"s so\n\t// the body starts and ends clean; this helper re-wraps with `\\n`\n\t// + body + `\\n\\n` below.\n\ttext = strings.TrimLeft(text, \"\\n\")\n\tif text == \"\" {\n\t\treturn \"\"\n\t}\n\t// Strip ALL trailing newlines so the loop produces exactly one\n\t// `\u003e line` per content line, then append `\\n\\n` at the end so the\n\t// blockquote terminates cleanly (see \"Output shape\" above).\n\ttext = strings.TrimRight(text, \"\\n\")\n\tif text == \"\" {\n\t\treturn \"\"\n\t}\n\tvar sb strings.Builder\n\t// Leading \"\\n\" so `chrome + BlockquoteRich(user)` cannot land the\n\t// first `\u003e` mid-line.\n\tsb.WriteByte('\\n')\n\tfor _, line := range strings.Split(text, \"\\n\") {\n\t\tsb.WriteString(\"\u003e \")\n\t\tsb.WriteString(line)\n\t\tsb.WriteByte('\\n')\n\t}\n\t// Trailing blank line so `BlockquoteRich(user) + chrome` cannot\n\t// pull chrome into the quote via lazy continuation.\n\tsb.WriteByte('\\n')\n\treturn sb.String()\n}\n\n// LinkTitle prepares user content for a CommonMark link-title or\n// image-title slot — the optional quoted text after the URL in any of\n// these forms:\n//\n//\t[text](url \"TITLE\")\n//\t![alt](src \"TITLE\")\n//\t[label]: url \"TITLE\"\n//\n// Escapes the inline-active set plus `\"` and `'` (the title delimiters\n// that aren't already in the inline set; `(` and `)` are), so the\n// caller can choose any of the three title-quote styles safely.\n//\n// Pick the right helper for the slot — markdown title and HTML\n// attribute share the look but use different escape rules:\n//\n//\t[text](url \"X\")              → LinkTitle      (markdown title)\n//\t\u003ca title=\"X\"\u003e                → HTMLEscape     (HTML attribute)\n//\t\u003ch5\u003eX\u003c/h5\u003e                   → HTMLEscape     (HTML element body)\n//\n// Swapping HTMLEscape for LinkTitle is wrong: HTML's `\u0026amp;` written\n// inside a markdown title renders as the literal characters `\u0026amp;`.\n// Swapping LinkTitle for HTMLEscape is wrong: markdown's `\\\"` survives\n// into the rendered HTML as a literal backslash-quote.\n//\n// Not idempotent (see package doc).\nfunc LinkTitle(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\ts = markdown.NormalizeBreaks(s)\n\ts = foldNewlinesAndSeparators(s, ' ')\n\treturn markdown.EscapeTitle(s)\n}\n\n// TableCell prepares user content for a GFM table cell — the bytes\n// between two `|` column delimiters in a table row like\n// `| cell-a | cell-b | cell-c |`. An unescaped `|` inside cell\n// content would open a new column, letting a malicious user shift\n// every column to its right.\n//\n// On top of InlineText's behavior, TableCell:\n//   - escapes `|` to `\\|` so user content can't end the cell early.\n//   - replaces tabs with single spaces. CommonMark expands tabs to\n//     the next multiple-of-4 column boundary (variable 1-4 spaces),\n//     which would shift the displayed cell-content width unpredictably\n//     and confuse table alignment.\n//\n// Not idempotent (see package doc).\nfunc TableCell(s string) string {\n\ts = InlineText(s)\n\ts = strings.ReplaceAll(s, \"\\t\", \" \")\n\ts = strings.ReplaceAll(s, \"|\", `\\|`)\n\treturn s\n}\n\n// HTMLEscape prepares user content for an HTML lexical slot inside\n// markdown — covers attribute values, element bodies, and HTML\n// comment bodies:\n//\n//\t\u003cgno-card type=\"...\" caption=\"X\"\u003e         attribute value\n//\t\u003cgno-alert title=\"X\"\u003e                     attribute value\n//\t\u003ch5\u003eX\u003c/h5\u003e                                element body\n//\t\u003cdetails\u003e\u003csummary\u003eX\u003c/summary\u003e...          element body\n//\t\u003c!-- X --\u003e                                comment body (safe: `\u003e`\n//\t                                          becomes `\u0026gt;`, so user\n//\t                                          cannot inject `--\u003e`)\n//\n// HTMLEscape escapes the union of attribute-breaking and body-breaking\n// characters (`\u003c`, `\u003e`, `\u0026`, `\"`, `'`), so one function safely serves\n// every HTML lexical context. Callers don't have to remember which\n// subset to use for which slot.\n//\n// Pick the right helper — markdown title and HTML attribute share\n// the look but use different escape rules:\n//\n//\t[text](url \"X\")              → LinkTitle      (markdown title)\n//\t\u003cspan title=\"X\"\u003e             → HTMLEscape     (HTML attribute)\n//\t\u003ch5\u003eX\u003c/h5\u003e                   → HTMLEscape     (HTML element body)\n//\n// Swapping InlineText for HTMLEscape is wrong: markdown's backslash\n// escapes survive into the rendered HTML as literal `\\*`. Swapping\n// LinkTitle for HTMLEscape is also wrong: `\u0026amp;` written inside a\n// markdown title renders as the literal characters `\u0026amp;`.\n//\n// Not idempotent (see package doc): calling twice produces\n// `\u0026amp;` → `\u0026amp;amp;`.\nfunc HTMLEscape(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\ts = markdown.NormalizeBreaks(s)\n\ts = foldNewlinesAndSeparators(s, ' ')\n\ts = replaceNULWithFFFD(s)\n\treturn html.EscapeString(s)\n}\n\n// ----- URL filters -----\n\n// URL validates a URL for use as a link href, percent-encodes unsafe\n// bytes, and rejects anything outside the allowlist of schemes.\n//\n// Allowlist:\n//   - http, https\n//   - mailto (rejected if it carries any query: prefill phishing via\n//     body, subject, cc, etc. Both '?' and '\u0026' are rejected; see\n//     linkSchemeAllowed for why '\u0026' counts.)\n//   - any URL WITHOUT a scheme — relative paths (`/path`, `./rel`,\n//     `bare-path`), query-only (`?q=v`), fragment-only (`#anchor`).\n//     A `:` appearing inside the URL (e.g. `/path:foo`, `?q=a:b`) is\n//     NOT a scheme separator per RFC 3986 — only `:` immediately after\n//     a leading `[a-zA-Z][a-zA-Z0-9+.-]*` counts.\n//\n// Rejected (have an unknown scheme):\n//   - javascript:, data:, vbscript:, blob:, file:, etc.\n//   - `//host/...` (protocol-relative — tracking-pixel vector)\n//\n// Returns \"\" if the URL is empty after trim or fails the allowlist.\nfunc URL(s string) string {\n\ts = strings.TrimSpace(s)\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\tif !linkSchemeAllowed(s) {\n\t\treturn \"\"\n\t}\n\treturn markdown.PercentEncodeURL(s)\n}\n\n// ImageURL validates a URL for use as an image src. Kept separate from\n// URL — not a parameterized variant — because the allowlist shapes\n// differ qualitatively (data:image/* vs. mailto:) and a single boolean\n// flag would invite callers to pass the wrong default.\n//\n// Allowlist:\n//   - http, https\n//   - schemeless relative URLs starting with /, ./, or ..\n//     (rejects // protocol-relative — tracking-pixel vector)\n//   - data:image/svg+xml, data:image/png, data:image/jpeg,\n//     data:image/gif, data:image/webp\n//\n// Any other data: subtype is rejected — data:text/html etc. would\n// render as inline HTML and execute embedded scripts.\n//\n// DEPLOYMENT PRECONDITION: data: URIs encode the bytes of the image\n// directly into the markup, so a malicious sender can construct an\n// image whose pixel dimensions are arbitrarily large at minimal byte\n// cost. The deploying gnoweb instance MUST clamp rendered image\n// dimensions via CSS (e.g. `max-width: 100%; max-height: \u003cbound\u003e`).\n// Without that cap, a single image can blow out the page layout or\n// exhaust the browser's memory.\n//\n// Returns \"\" if the URL is empty after trim or fails the allowlist.\nfunc ImageURL(s string) string {\n\ts = strings.TrimSpace(s)\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\tif !imageSchemeAllowed(s) {\n\t\treturn \"\"\n\t}\n\treturn markdown.PercentEncodeURL(s)\n}\n\n// ----- Validators -----\n\n// userNameCharsets builds the [2]uint64 bitmaps for the r/sys/users\n// charset: first [a-z], rest [a-z0-9_-]. Initialized once at package\n// init.\nvar (\n\tuserNameFirstLo, userNameFirstHi           uint64\n\tuserNameRestLo, userNameRestHi             uint64\n\tfootnoteLabelFirstLo, footnoteLabelFirstHi uint64\n\tfootnoteLabelRestLo, footnoteLabelRestHi   uint64\n\tlangFirstLo, langFirstHi                   uint64\n\tlangRestLo, langRestHi                     uint64\n\tbechHrpFirstLo, bechHrpFirstHi             uint64\n\tbechHrpRestLo, bechHrpRestHi               uint64\n\tbechDataFirstLo, bechDataFirstHi           uint64\n\tbechDataRestLo, bechDataRestHi             uint64\n)\n\nfunc init() {\n\t// UserName: first [a-z], rest [a-z0-9_-].\n\tfor c := byte('a'); c \u003c= 'z'; c++ {\n\t\tsetBit(\u0026userNameFirstLo, \u0026userNameFirstHi, c)\n\t\tsetBit(\u0026userNameRestLo, \u0026userNameRestHi, c)\n\t}\n\tfor c := byte('0'); c \u003c= '9'; c++ {\n\t\tsetBit(\u0026userNameRestLo, \u0026userNameRestHi, c)\n\t}\n\tsetBit(\u0026userNameRestLo, \u0026userNameRestHi, '_')\n\tsetBit(\u0026userNameRestLo, \u0026userNameRestHi, '-')\n\n\t// FootnoteLabel: [A-Za-z0-9_-] for both first and rest.\n\tfor c := byte('A'); c \u003c= 'Z'; c++ {\n\t\tsetBit(\u0026footnoteLabelFirstLo, \u0026footnoteLabelFirstHi, c)\n\t\tsetBit(\u0026footnoteLabelRestLo, \u0026footnoteLabelRestHi, c)\n\t}\n\tfor c := byte('a'); c \u003c= 'z'; c++ {\n\t\tsetBit(\u0026footnoteLabelFirstLo, \u0026footnoteLabelFirstHi, c)\n\t\tsetBit(\u0026footnoteLabelRestLo, \u0026footnoteLabelRestHi, c)\n\t}\n\tfor c := byte('0'); c \u003c= '9'; c++ {\n\t\tsetBit(\u0026footnoteLabelFirstLo, \u0026footnoteLabelFirstHi, c)\n\t\tsetBit(\u0026footnoteLabelRestLo, \u0026footnoteLabelRestHi, c)\n\t}\n\tfor _, c := range []byte{'_', '-'} {\n\t\tsetBit(\u0026footnoteLabelFirstLo, \u0026footnoteLabelFirstHi, c)\n\t\tsetBit(\u0026footnoteLabelRestLo, \u0026footnoteLabelRestHi, c)\n\t}\n\n\t// LanguageName: [a-zA-Z0-9_+-] for both first and rest.\n\tfor c := byte('A'); c \u003c= 'Z'; c++ {\n\t\tsetBit(\u0026langFirstLo, \u0026langFirstHi, c)\n\t\tsetBit(\u0026langRestLo, \u0026langRestHi, c)\n\t}\n\tfor c := byte('a'); c \u003c= 'z'; c++ {\n\t\tsetBit(\u0026langFirstLo, \u0026langFirstHi, c)\n\t\tsetBit(\u0026langRestLo, \u0026langRestHi, c)\n\t}\n\tfor c := byte('0'); c \u003c= '9'; c++ {\n\t\tsetBit(\u0026langFirstLo, \u0026langFirstHi, c)\n\t\tsetBit(\u0026langRestLo, \u0026langRestHi, c)\n\t}\n\tfor _, c := range []byte{'_', '+', '-'} {\n\t\tsetBit(\u0026langFirstLo, \u0026langFirstHi, c)\n\t\tsetBit(\u0026langRestLo, \u0026langRestHi, c)\n\t}\n\n\t// Bech HRP (when prefix==\"\"): [a-z], 1-16 chars.\n\tfor c := byte('a'); c \u003c= 'z'; c++ {\n\t\tsetBit(\u0026bechHrpFirstLo, \u0026bechHrpFirstHi, c)\n\t\tsetBit(\u0026bechHrpRestLo, \u0026bechHrpRestHi, c)\n\t}\n\n\t// Bech data part: [a-z0-9], 6-90 chars.\n\tfor c := byte('a'); c \u003c= 'z'; c++ {\n\t\tsetBit(\u0026bechDataFirstLo, \u0026bechDataFirstHi, c)\n\t\tsetBit(\u0026bechDataRestLo, \u0026bechDataRestHi, c)\n\t}\n\tfor c := byte('0'); c \u003c= '9'; c++ {\n\t\tsetBit(\u0026bechDataFirstLo, \u0026bechDataFirstHi, c)\n\t\tsetBit(\u0026bechDataRestLo, \u0026bechDataRestHi, c)\n\t}\n}\n\nfunc setBit(lo, hi *uint64, c byte) {\n\tif c \u003c 64 {\n\t\t*lo |= 1 \u003c\u003c c\n\t} else {\n\t\t*hi |= 1 \u003c\u003c (c - 64)\n\t}\n}\n\n// UserName validates the r/sys/users-registration charset:\n// ^[a-z][a-z0-9]*([_-][a-z0-9]+)*$ length ≤ 64.\n//\n// The native MatchCharsetN enforces the leading-letter + tail-charset\n// shape and length bound; this helper also performs the bidi-strip\n// pre-pass. The \"no consecutive [_-]\" rule from r/sys/users is NOT\n// enforced here (it's a registration-policy rule, not a sanitization\n// concern — registrations go through r/sys/users itself).\n//\n// Returns the (bidi-stripped) input if valid, \"\" otherwise. On a \"\"\n// return, do not emit the user-mention markup at all (e.g. skip the\n// `[@user](/u/user)` link); falling back to the raw user-supplied\n// string would defeat the validation.\nfunc UserName(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\tif markdown.MatchCharsetN(s, userNameFirstLo, userNameFirstHi, userNameRestLo, userNameRestHi, 1, 64) {\n\t\treturn s\n\t}\n\treturn \"\"\n}\n\n// BechString validates a bech32-style address-like string.\n//\n// A bech32 string has the shape `\u003chrp\u003e1\u003cdata\u003e`: a human-readable\n// prefix (HRP) that names the family (e.g. `g` for gno addresses,\n// `gpub` for gno pubkeys, `cosmos` for cosmos addresses), the\n// separator character `1`, then a data part carrying the encoded\n// payload as lowercase alphanumerics.\n//\n// If prefix != \"\", requires s to start with prefix+\"1\" exactly, and the\n// data part to match ^[a-z0-9]{6,90}$. Use this when you know the\n// expected family:\n//\n//\tsanitize.BechString(addr, \"g\")     // only g1...     (addresses)\n//\tsanitize.BechString(pk,   \"gpub\")  // only gpub1...  (pubkeys)\n//\n// If prefix == \"\", accepts any reasonable bech32 shape:\n// ^[a-z]{1,16}1[a-z0-9]{6,90}$.\n//\n// Syntactic only — does NOT verify the bech32 checksum. Use a true\n// bech32 decoder if you need that. Returns the cleaned input on\n// accept, \"\" on reject; on \"\" return, do not emit the address-link\n// markup (the user-supplied bytes have failed shape validation and\n// should not appear unmodified in output).\nfunc BechString(s, prefix string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\tif prefix != \"\" {\n\t\t// HRP must be lowercase ASCII letters.\n\t\tfor i := 0; i \u003c len(prefix); i++ {\n\t\t\tc := prefix[i]\n\t\t\tif c \u003c 'a' || c \u003e 'z' {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t}\n\t\tneed := prefix + \"1\"\n\t\tif !strings.HasPrefix(s, need) {\n\t\t\treturn \"\"\n\t\t}\n\t\tdata := s[len(need):]\n\t\tif markdown.MatchCharsetN(data, bechDataFirstLo, bechDataFirstHi, bechDataRestLo, bechDataRestHi, 6, 90) {\n\t\t\treturn s\n\t\t}\n\t\treturn \"\"\n\t}\n\t// prefix == \"\" — accept any 1-16 char lowercase HRP, then '1', then data.\n\tsep := strings.IndexByte(s, '1')\n\tif sep \u003c 1 || sep \u003e 16 {\n\t\treturn \"\"\n\t}\n\thrp := s[:sep]\n\tif !markdown.MatchCharsetN(hrp, bechHrpFirstLo, bechHrpFirstHi, bechHrpRestLo, bechHrpRestHi, 1, 16) {\n\t\treturn \"\"\n\t}\n\tdata := s[sep+1:]\n\tif markdown.MatchCharsetN(data, bechDataFirstLo, bechDataFirstHi, bechDataRestLo, bechDataRestHi, 6, 90) {\n\t\treturn s\n\t}\n\treturn \"\"\n}\n\n// FootnoteLabel validates an identifier used as a footnote name, link-\n// reference-definition label, or {#id} anchor: ^[A-Za-z0-9_-]{1,64}$.\n// Strips bidi/zero-width first. Returns s if valid, \"\" otherwise.\n//\n// Use for every shape where a markdown identifier is treated as an\n// opaque key by the parser:\n//\n//   - footnote-definition labels:        [^FootnoteLabel(name)]: body\n//   - footnote-reference labels:         see [^FootnoteLabel(name)]\n//   - link-reference-definition labels:  [FootnoteLabel(label)]: url\n//   - reference-link USE labels:         [text][FootnoteLabel(label)]\n//   - goldmark auto-anchor {#id}:        # Heading {#FootnoteLabel(id)}\n//\n// The shared validator name reflects the shared charset and shared\n// security goal — keep untrusted bytes out of any parser-managed\n// identifier slot.\n//\n// On \"\" return, omit the footnote / LRD / anchor entirely rather than\n// emitting it with raw user bytes.\nfunc FootnoteLabel(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\tif markdown.MatchCharsetN(s, footnoteLabelFirstLo, footnoteLabelFirstHi, footnoteLabelRestLo, footnoteLabelRestHi, 1, 64) {\n\t\treturn s\n\t}\n\treturn \"\"\n}\n\n// LanguageName validates the language tag (a.k.a. \"info string\") for\n// a fenced code block — the `go` in:\n//\n//\t```go\n//\tfmt.Println(\"hi\")\n//\t```\n//\n// Charset: ^[a-zA-Z0-9_+-]{1,32}$ — letters, digits, `_`, `+`, `-`,\n// up to 32 bytes. Strips bidi/zero-width first.\n//\n// Returns the cleaned input if valid, \"\" otherwise. A \"\" return means\n// the caller should emit a language-less fence (``` without a tag)\n// rather than letting the user pick the syntax highlighter — which\n// could otherwise be used to inject newlines or block markers into\n// what becomes the opening fence line.\nfunc LanguageName(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\tif markdown.MatchCharsetN(s, langFirstLo, langFirstHi, langRestLo, langRestHi, 1, 32) {\n\t\treturn s\n\t}\n\treturn \"\"\n}\n\n// NestedPrefix validates a prefix string for line-prefixing builders\n// like md.Nested, which prepends `prefix` to every line of content\n// to render the content as a nested/indented sub-block.\n//\n// Allowed: any string matching `^[ \\t\u003e]*$` — spaces, tabs, blockquote\n// `\u003e` chars only. Anything else (a `#`, a `-`, a letter) would let a\n// caller turn benign sub-content into a heading, list, or paragraph\n// at the wrong nesting level.\n//\n// Returns s if valid, \"\" otherwise. Strips bidi/zero-width first —\n// otherwise an invisible character hidden inside a `\u003e` prefix would\n// be replicated on every nested content line, producing per-line\n// display-vs-storage divergence.\n//\n// On \"\" return, fall back to a known-safe prefix literal (e.g.\n// `\"\u003e \"`) or skip the nesting entirely. Do not emit the raw\n// user-supplied prefix.\nfunc NestedPrefix(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tc := s[i]\n\t\tif c != ' ' \u0026\u0026 c != '\\t' \u0026\u0026 c != '\u003e' {\n\t\t\treturn \"\"\n\t\t}\n\t}\n\treturn s\n}\n\n// ----- Primitive -----\n\n// CodeFence returns a string of backticks long enough to wrap content\n// as a CommonMark fenced code block without the content's own backticks\n// closing the fence prematurely.\n//\n// Returned length N = max(minCount, longestBacktickRunInContent + 1).\n// Use N backticks both before and after the content:\n//\n//\tfence := sanitize.CodeFence(userCode, 3)\n//\tout += fence + \"\\n\" + userCode + \"\\n\" + fence + \"\\n\"\n//\n// Typical minCount values:\n//   - 1 for inline code spans (`x`)\n//   - 3 for block fenced code (CommonMark §4.5 requires ≥3)\n//\n// `minCount \u003c 1` is clamped to 1. Empty content returns\n// strings.Repeat(\"`\", max(minCount, 1)). Never panics.\n//\n// Most realms should reach for InlineCode / CodeBlock /\n// LanguageCodeBlock below, which call CodeFence internally and emit\n// the full code block for you. Call CodeFence directly only when\n// you're rolling a custom fence emitter (e.g. a renderer that needs\n// the fence length but emits the body differently).\nfunc CodeFence(content string, minCount int) string {\n\treturn markdown.CodeFence(content, minCount)\n}\n\n// InlineCode wraps user content as a CommonMark inline code span — the\n// `code` in “ `code` “. Use for any user-derived token, identifier,\n// or short literal that should render in monospace inside running\n// prose: variable names, hashes, hex addresses, token symbols, error\n// codes, package paths, transaction IDs.\n//\n// Inline code spans cannot span lines (a `\\n` inside the content would\n// end the span and leave the surrounding backticks as literal text),\n// so all line breaks — CR / CRLF / LF, NEL (U+0085), U+2028, U+2029 —\n// are folded to a single space. If you want each line of user content\n// on its own row, use CodeBlock instead.\n//\n// Behavior:\n//   - Bidi/zero-width controls are stripped (browsers honor bidi marks\n//     inside `\u003ccode\u003e`, so leaving them would let stored bytes display\n//     as something different).\n//   - NUL is replaced with U+FFFD.\n//   - The wrapping fence is one backtick longer than the longest\n//     backtick run in the content, so internal backticks can never\n//     close the span prematurely.\n//   - A single space pad is added on each side when content starts or\n//     ends with “ ` “ or space, so leading/trailing backticks render\n//     literally rather than fusing with the fence (the renderer\n//     strips one space from each side per CommonMark spec).\n//\n// Empty input returns \"\" rather than a literal two-backtick string\n// (which CommonMark parses as text, not as an empty code span). If\n// you use InlineCode as link text and it returns \"\", omit the link\n// entirely.\n//\n// Not idempotent (see package doc): wraps with a fence, so calling\n// twice double-wraps.\nfunc InlineCode(content string) string {\n\tcontent = markdown.StripBidiAndZeroWidth(content)\n\tcontent = markdown.NormalizeBreaks(content)\n\tcontent = foldNewlinesAndSeparators(content, ' ')\n\tcontent = replaceNULWithFFFD(content)\n\tif content == \"\" {\n\t\treturn \"\"\n\t}\n\tfence := markdown.CodeFence(content, 1)\n\tpad := \"\"\n\tif content[0] == '`' || content[0] == ' ' ||\n\t\tcontent[len(content)-1] == '`' || content[len(content)-1] == ' ' {\n\t\tpad = \" \"\n\t}\n\treturn fence + pad + content + pad + fence\n}\n\n// CodeBlock wraps user content as a CommonMark fenced code block.\n// Use for any user-derived multi-line snippet that should render as a\n// code block: log excerpts, JSON dumps, error backtraces, config\n// snippets, posted code samples.\n//\n// Behavior:\n//   - Bidi/zero-width controls are stripped.\n//   - CR/CRLF line endings are normalized to LF; Unicode separators\n//     (NEL U+0085, U+2028, U+2029) are folded to LF for line-count\n//     consistency.\n//   - NUL is replaced with U+FFFD per CM §2.3.\n//   - The wrapping fence is at least 3 backticks (CM §4.5 minimum) and\n//     sized to outscan internal backticks — an attacker cannot embed\n//     a closing fence in the content.\n//\n// Empty content emits an empty fenced block (\"```\\n\\n```\\n\"), which is\n// valid CommonMark and renders as an empty `\u003cpre\u003e\u003ccode\u003e\u003c/code\u003e\u003c/pre\u003e`.\n//\n// Not idempotent (see package doc).\nfunc CodeBlock(content string) string {\n\tcontent = markdown.StripBidiAndZeroWidth(content)\n\tcontent = markdown.NormalizeBreaks(content)\n\tcontent = foldNewlinesAndSeparators(content, '\\n')\n\tcontent = replaceNULWithFFFD(content)\n\tfence := markdown.CodeFence(content, 3)\n\treturn fence + \"\\n\" + content + \"\\n\" + fence + \"\\n\"\n}\n\n// LanguageCodeBlock wraps user content as a fenced code block tagged\n// with a programming-language hint (the \"info string\" after the\n// opening fence, e.g. `go` in ```` ```go ````) so the renderer can\n// apply syntax highlighting.\n//\n// An invalid `language` tag silently falls back to a tagless fence —\n// the helper never returns an error or panics. If a realm author is\n// debugging \"why is my Go highlighting gone?\", the input failed the\n// language validator (charset ^[a-zA-Z0-9_+-]{1,32}$ after bidi-strip).\n// This fallback exists because an unvalidated tag could contain a\n// newline that injects content (e.g. a heading) onto what becomes the\n// opening fence line.\n//\n// Content is cleaned exactly as in CodeBlock (bidi-strip, CR/CRLF\n// normalize to LF, NEL/U+2028/U+2029 fold to LF, NUL→U+FFFD, fence\n// sized to outscan internal backticks).\n//\n// Not idempotent (see package doc).\nfunc LanguageCodeBlock(language, content string) string {\n\tcontent = markdown.StripBidiAndZeroWidth(content)\n\tcontent = markdown.NormalizeBreaks(content)\n\tcontent = foldNewlinesAndSeparators(content, '\\n')\n\tcontent = replaceNULWithFFFD(content)\n\tfence := markdown.CodeFence(content, 3)\n\tlang := LanguageName(language) // \"\" on reject\n\treturn fence + lang + \"\\n\" + content + \"\\n\" + fence + \"\\n\"\n}\n\n// ----- Reference-style definitions -----\n\n// FootnoteDefinition emits a GFM footnote definition — the\n// `[^name]: body` form that introduces a footnote whose body is rendered\n// in the page footer (or wherever the renderer chooses to place it).\n// Other parts of the markdown reference the footnote by writing\n// `[^name]` inline.\n//\n// Use for any realm-rendered footnote where the body text comes from\n// user input. The realm picks the footnote name (passed as `name`,\n// validated by FootnoteLabel — failure here returns \"\"); the user's\n// content goes in `text`, which is sanitized via Block.\n//\n// Contract:\n//   - `name`: passed raw, validated as a FootnoteLabel\n//     (^[A-Za-z0-9_-]{1,64}$). Reject → return \"\".\n//   - `text`: passed raw multi-paragraph user prose, cleaned via Block\n//     (bidi-strip, line-ending normalize, LRD strip, block-marker\n//     escape, ref-link USE escape, fence auto-close).\n//\n// Empty body → returns \"\" (a label without body is not a valid\n// footnote definition; the markdown would parse as a paragraph\n// containing the label).\n//\n// Output shape:\n//\n//\t[^name]:\n//\t    line 1 of body\n//\t    line 2 of body\n//\t    ...\n//\n// The label sits on its own line and each body line gets a 4-space\n// indent — the GFM continuation rule that keeps multi-paragraph body\n// text bound to the footnote rather than detaching as a new paragraph.\n//\n// Not idempotent (see package doc): composes Block internally; passing\n// already-sanitized body text double-escapes.\nfunc FootnoteDefinition(name, text string) string {\n\tlabel := FootnoteLabel(name)\n\tif label == \"\" {\n\t\treturn \"\"\n\t}\n\t// Block now wraps with \"\\n\\n\" on both sides for cross-paragraph\n\t// isolation; inside a footnote-definition's 4-space-indented body\n\t// the wrap would line-prefix to blank padding lines, so strip ALL\n\t// leading and trailing \"\\n\"s before continuation-indenting.\n\tbody := strings.Trim(Block(text), \"\\n\")\n\tif body == \"\" {\n\t\treturn \"\"\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"[^\")\n\tb.WriteString(label)\n\tb.WriteString(\"]:\\n\")\n\tfor _, line := range strings.Split(body, \"\\n\") {\n\t\tif line == \"\" {\n\t\t\tb.WriteByte('\\n')\n\t\t} else {\n\t\t\tb.WriteString(\"    \")\n\t\t\tb.WriteString(line)\n\t\t\tb.WriteByte('\\n')\n\t\t}\n\t}\n\treturn b.String()\n}\n\n// LinkReferenceDefinition emits a CommonMark link reference definition\n// (CM §4.7) — the `[label]: url \"title\"` form that other parts of the\n// markdown reference by writing `[text][label]` or `[label]` (shortcut).\n//\n// Use for any realm-rendered LRD where the realm owns the label but\n// any of the URL or title come from user input. The user content for\n// the URL goes through URL (allowlist-based — reject → \"\"); the title\n// goes through LinkTitle (escape).\n//\n// Contract:\n//   - `label`: passed raw, validated as a FootnoteLabel\n//     (^[A-Za-z0-9_-]{1,64}$). Realms should choose a namespaced label\n//     using dashes (e.g. `r-myrealm-help`) so shortcut-reference\n//     invocations from user content can't collide with bare prose\n//     (`[help]`, `[click here]`). `/` is not in the FootnoteLabel\n//     charset; reject → return \"\".\n//   - `url`: passed raw, sanitized via URL. If URL rejects, the LRD is\n//     skipped (return \"\").\n//   - `title`: passed raw, sanitized via LinkTitle. Empty title → no\n//     title clause emitted.\n//\n// The output is framed with leading and trailing blank lines so that\n// the definition cannot accidentally fuse with adjacent paragraph\n// content into a setext underline or a continuation line.\n//\n// Not idempotent (see package doc).\nfunc LinkReferenceDefinition(label, url, title string) string {\n\tlbl := FootnoteLabel(label)\n\tif lbl == \"\" {\n\t\treturn \"\"\n\t}\n\tsafeURL := URL(url)\n\tif safeURL == \"\" {\n\t\treturn \"\"\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"\\n\\n[\")\n\tb.WriteString(lbl)\n\tb.WriteString(\"]: \")\n\tb.WriteString(safeURL)\n\tif title != \"\" {\n\t\tb.WriteString(\" \\\"\")\n\t\tb.WriteString(LinkTitle(title))\n\t\tb.WriteString(\"\\\"\")\n\t}\n\tb.WriteString(\"\\n\\n\")\n\treturn b.String()\n}\n\n// ----- internal helpers -----\n\n// linkSchemeAllowed returns true if s passes the URL helper's scheme\n// allowlist. See URL's doc for the policy.\nfunc linkSchemeAllowed(s string) bool {\n\tif strings.HasPrefix(s, \"http://\") || strings.HasPrefix(s, \"https://\") {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"mailto:\") {\n\t\t// Reject any query: RFC 6068 headers (body, subject, cc, bcc, ...)\n\t\t// prefill the composed message and are a phishing vector. '?' opens\n\t\t// the header section (covering percent-encoded names like ?%62ody=).\n\t\t// '\u0026' is rejected because gnoweb's renderer decodes HTML character\n\t\t// references ('\u0026#63;', '\u0026#x3f;', '\u0026quest;') back into '?' after this\n\t\t// check, reconstituting a query. Percent-encoded '%3f' stays encoded\n\t\t// and reads as a literal '?' in the address, so it's allowed.\n\t\tif strings.ContainsAny(s, \"?\u0026\") {\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"//\") {\n\t\t// Protocol-relative — reject (tracking-pixel vector).\n\t\treturn false\n\t}\n\t// Any URL with an unknown scheme (RFC 3986: `^[a-zA-Z][a-zA-Z0-9+.-]*:`)\n\t// is rejected — this blocks `javascript:`, `data:`, `vbscript:`, `blob:`,\n\t// and anything else not handled above. URLs without a scheme are\n\t// treated as relative and accepted (bare path, query-only, fragment).\n\tif hasURLScheme(s) {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// hasURLScheme reports whether s begins with a scheme followed by ':'\n// per RFC 3986 (^[a-zA-Z][a-zA-Z0-9+.-]*:). A `:` appearing later in\n// the URL (e.g. `/path:foo` or `?q=a:b`) does not count.\nfunc hasURLScheme(s string) bool {\n\tif len(s) == 0 {\n\t\treturn false\n\t}\n\tc := s[0]\n\tif !((c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z')) {\n\t\treturn false\n\t}\n\tfor i := 1; i \u003c len(s); i++ {\n\t\tc := s[i]\n\t\tif c == ':' {\n\t\t\treturn true\n\t\t}\n\t\tif !((c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') ||\n\t\t\t(c \u003e= '0' \u0026\u0026 c \u003c= '9') || c == '+' || c == '.' || c == '-') {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn false\n}\n\n// imageSchemeAllowed returns true if s passes the ImageURL helper's\n// scheme allowlist. Tighter than linkSchemeAllowed: no mailto/tel,\n// only data:image/\u003csubset\u003e.\nfunc imageSchemeAllowed(s string) bool {\n\tif strings.HasPrefix(s, \"http://\") || strings.HasPrefix(s, \"https://\") {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"//\") {\n\t\treturn false\n\t}\n\tif strings.HasPrefix(s, \"/\") || strings.HasPrefix(s, \"./\") || strings.HasPrefix(s, \"../\") {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"data:\") {\n\t\t// Only the curated image/* subset. CSS must enforce sizing.\n\t\tfor _, p := range []string{\n\t\t\t\"data:image/svg+xml\",\n\t\t\t\"data:image/png\",\n\t\t\t\"data:image/jpeg\",\n\t\t\t\"data:image/gif\",\n\t\t\t\"data:image/webp\",\n\t\t} {\n\t\t\tif strings.HasPrefix(s, p) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\treturn false\n}\n\n// foldNewlinesAndSeparators replaces \\n, U+0085 NEL, U+2028 LINE SEPARATOR,\n// U+2029 PARAGRAPH SEPARATOR with the given replacement byte (typically\n// space for inline-context helpers).\n//\n// NormalizeBreaks has already folded \\r\\n and \\r to \\n before this runs,\n// so \\n is the canonical break byte to substitute.\nfunc foldNewlinesAndSeparators(s string, replacement byte) string {\n\tif !needsSeparatorFold(s) {\n\t\treturn s\n\t}\n\tout := make([]byte, 0, len(s))\n\tfor i := 0; i \u003c len(s); {\n\t\tc := s[i]\n\t\tif c == '\\n' {\n\t\t\tout = append(out, replacement)\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\t\t// U+0085 NEL: 0xC2 0x85\n\t\tif c == 0xC2 \u0026\u0026 i+1 \u003c len(s) \u0026\u0026 s[i+1] == 0x85 {\n\t\t\tout = append(out, replacement)\n\t\t\ti += 2\n\t\t\tcontinue\n\t\t}\n\t\t// U+2028 (0xE2 0x80 0xA8) or U+2029 (0xE2 0x80 0xA9)\n\t\tif c == 0xE2 \u0026\u0026 i+2 \u003c len(s) \u0026\u0026 s[i+1] == 0x80 \u0026\u0026 (s[i+2] == 0xA8 || s[i+2] == 0xA9) {\n\t\t\tout = append(out, replacement)\n\t\t\ti += 3\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, c)\n\t\ti++\n\t}\n\treturn string(out)\n}\n\nfunc needsSeparatorFold(s string) bool {\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tc := s[i]\n\t\tif c == '\\n' || c == 0xC2 || c == 0xE2 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// replaceNULWithFFFD substitutes any NUL byte with the UTF-8 encoding\n// of U+FFFD REPLACEMENT CHARACTER per CM §2.3.\nfunc replaceNULWithFFFD(s string) string {\n\tif !strings.ContainsRune(s, 0) {\n\t\treturn s\n\t}\n\treturn strings.ReplaceAll(s, \"\\x00\", \"\\ufffd\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":"EO+66JXR6KnpMaToudFomCRcqjKRJYDRLOWnEsvBJi0rZz6c3HitxUQuMuu1TWs1TNTde1avjv6ZgOOW1dn/eA=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","package":{"name":"kourtv2","path":"gno.land/r/kourt/kourtv2","files":[{"name":"adminparams.gno","body":"// One place to see every knob an administrator can turn, and what it is set to.\n//\n// WHY THIS EXISTS. The realm-wide settings were readable one at a time and\n// nowhere together: AssociationBondDefault here, SiteDomain there, MediaHosts\n// somewhere else, the ladder and the standing rates only through a per-court\n// read that resolves an override first, and the purge threshold not at all. So\n// the question \"what is this realm configured to right now\" had no answer short\n// of nine queries and knowing which nine — which means in practice nobody asked\n// it, and a setting left at a value nobody intended could sit there unnoticed.\n//\n// SEVEN PARAMETERS, and the boundary is exactly \"the global DAO admin can set\n// it\". Per-court knobs are deliberately not here: SetCourtAssociationBond,\n// SetCourtLadder and SetCourtCreditRates are a court moderator's business, they\n// belong on that court's own page, and mixing the two would misrepresent who\n// can change what. SetTier is also left out — it is gated on isGlobalMod, a\n// wider set than the admin, and listing it under \"admin params\" would be wrong\n// about its authority.\n//\n// ONE LINE, and that is a constraint rather than a preference: the overlay's\n// parseTyped splits raw output on newlines before it matches a typed value, so\n// a multi-line answer parses as garbage (media.gno's MediaHosts says the same,\n// for the same reason). Hence a flat `key:value;key:value` string, with the\n// ladder and rates FLATTENED rather than nested — their own packed forms use\n// `;` internally, and nesting one inside the other would make the separator\n// ambiguous for anything reading this.\n//\n// VALUES ARE READ, NEVER RESTATED. Every field below resolves through the same\n// global the setter writes, so this page cannot drift from the realm: a listing\n// that keeps its own copy of a default is a listing that is wrong the first\n// time somebody changes one.\npackage kourtv2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n/* PURE READS OF THE DAO, because the ensure* builder ALLOCATES. It builds the\n   DAO on first call, and check-read-purity refuses that inside an exported read\n   for a good reason: a query carries no storage deposit and must be safe to\n   serve at any height, so a read that allocates makes the first caller change\n   what the second one sees. AdminParams was written with the builder and the\n   gate caught it.\n   Before the DAO is built these report what it WOULD be built with — admin from\n   directoryAdmin, which is exactly what the builder copies, and the named\n   bootstrap size from moderation.gno — so the listing is correct rather than\n   empty on a realm whose DAO nobody has touched yet. */\nfunc adminAddr() address {\n\tif globalDAO != nil {\n\t\treturn globalDAO.admin\n\t}\n\treturn directoryAdmin\n}\n\nfunc purgeThreshold() int {\n\tif globalDAO != nil {\n\t\treturn globalDAO.purgeM\n\t}\n\treturn daoBootstrapM\n}\n\nfunc daoSeats() int {\n\tif globalDAO != nil {\n\t\treturn globalDAO.n\n\t}\n\treturn daoBootstrapN\n}\n\n// AdminParams packs every realm-wide, admin-settable parameter and its live\n// value, one line, `key:value` pairs separated by semicolons:\n//\n//\tadmin:\u003caddress\u003e;creationBurn:\u003cugnot\u003e;assocBond:\u003cugnot\u003e;purgeM:\u003cn\u003e;\n//\tt1:\u003cbps\u003e;t2:\u003cbps\u003e;t3:\u003cbps\u003e;entry:\u003cn\u003e;l2:\u003cn\u003e;l3:\u003cn\u003e;\n//\tflag:\u003cbps\u003e;dispute:\u003cbps\u003e;authorHigh:\u003cbps\u003e;conviction:\u003cbps\u003e;unadjudicated:\u003cbps\u003e;\n//\tdomain:\u003chost\u003e;mediaExact:\u003ccsv\u003e;mediaSuffix:\u003ccsv\u003e\n//\n// (written across lines here for reading; the value is a single line.)\n//\n// A reader splitting this must split each pair on the FIRST colon only: the\n// media lists are comma-separated and a future host could contain one, and\n// SplitN costs nothing today against a parse that silently truncates later.\nfunc AdminParams() string {\n\tl := ladderDefault\n\tr := creditRatesDefault\n\thosts := MediaHosts()\n\texact, suffix := hosts, \"\"\n\tif i := strings.Index(hosts, \"|\"); i \u003e= 0 {\n\t\texact, suffix = hosts[:i], hosts[i+1:]\n\t}\n\tn := func(v int64) string { return strconv.FormatInt(v, 10) }\n\treturn \"admin:\" + adminAddr().String() +\n\t\t\";creationBurn:\" + n(courtCreationBurn) +\n\t\t\";assocBond:\" + n(assocBondDefault) +\n\t\t\";purgeM:\" + strconv.Itoa(purgeThreshold()) +\n\t\t\";t1:\" + n(l.t1Bps) +\n\t\t\";t2:\" + n(l.t2Bps) +\n\t\t\";t3:\" + n(l.t3Bps) +\n\t\t\";entry:\" + n(l.postsEntry) +\n\t\t\";l2:\" + n(l.postsT2) +\n\t\t\";l3:\" + n(l.postsT3()) +\n\t\t\";flag:\" + n(r.flag) +\n\t\t\";dispute:\" + n(r.dispute) +\n\t\t\";authorHigh:\" + n(r.authorHigh) +\n\t\t\";conviction:\" + n(r.conviction) +\n\t\t\";unadjudicated:\" + n(r.unadjudicated) +\n\t\t\";domain:\" + siteDomain +\n\t\t\";mediaExact:\" + exact +\n\t\t\";mediaSuffix:\" + suffix\n}\n\n// adminParamsPath is the render route. TWELVE CHARACTERS, and that is load\n// bearing: a slug is at most maxSlugLen (11), so this path cannot collide with\n// any court that exists or could ever be created. help.gno reserves its own\n// route the same way and for the same reason — by construction rather than by a\n// reserved word somebody has to remember to keep reserved.\nconst adminParamsPath = \"admin-params\"\n\n// renderAdminParams draws the same seven parameters for a human.\n//\n// It reads the SAME globals AdminParams packs rather than parsing that string,\n// so the page and the machine read cannot disagree; what differs is only the\n// presentation. Each row says what the setting DOES, because a name and a\n// number together still do not tell an administrator what turning it would\n// change, and the verb that sets it, because the next question after \"what is\n// it\" is always \"how do I change it\".\nfunc renderAdminParams() string {\n\tl := ladderDefault\n\tr := creditRatesDefault\n\tn := func(v int64) string { return strconv.FormatInt(v, 10) }\n\n\tvar b strings.Builder\n\tb.WriteString(\"# Realm parameters\\n\\n\")\n\tb.WriteString(\"Everything the global DAO admin can set, and what it is set \" +\n\t\t\"to now. A court's moderators set their own court's bond, ladder and \" +\n\t\t\"standing rates; those are on the court's page, not here.\\n\\n\")\n\tb.WriteString(\"Admin: `\" + adminAddr().String() + \"`\\n\\n\")\n\n\tb.WriteString(\"| Setting | Now | What it does | Set with |\\n\")\n\tb.WriteString(\"|---|---|---|---|\\n\")\n\tb.WriteString(\"| Court creation burn | \" + ugnotAmt(courtCreationBurn) +\n\t\t\" GNOT | The minimum a new court costs to open. The whole payment is \" +\n\t\t\"burned. 0 turns it off, and off is the shipped default. | \" +\n\t\t\"`SetCourtCreationBurn` |\\n\")\n\tb.WriteString(\"| Association bond | \" + ccAmt(assocBondDefault) +\n\t\t\" CC | What a stranger posts to attach an edge, in courts that set no \" +\n\t\t\"override. | `SetAssociationBondDefault` |\\n\")\n\tb.WriteString(\"| Purge threshold | \" + strconv.Itoa(purgeThreshold()) + \" of \" +\n\t\tstrconv.Itoa(daoSeats()) + \" | How many global moderators must agree to purge. | \" +\n\t\t\"`SetPurgeThreshold` |\\n\")\n\tb.WriteString(\"| Posting ladder | \" + n(l.t1Bps) + \"/\" + n(l.t2Bps) + \"/\" +\n\t\tn(l.t3Bps) + \" bps at \" + n(l.postsEntry) + \"/\" + n(l.postsT2) + \"/\" +\n\t\tn(l.postsT3()) + \" posts | The default rungs a court inherits: the share \" +\n\t\t\"earned at each tier and the post counts that reach them. | \" +\n\t\t\"`SetLadderDefault` |\\n\")\n\t/* THE FLAG RATE IS INERT AND THE PAGE SAYS SO. Every other rate here is\n\t   paid by a live path; flag's was creditFlagSlash, which went with the\n\t   quality lane, so nothing in the realm credits it today. Listing it under\n\t   \"the credit each act earns\" without that word would tell an administrator\n\t   a knob works when it does not — and it is worse than a dead constant,\n\t   because mustCreditRates still refuses authorship set above it. */\n\tb.WriteString(\"| Standing rates | flag \" + n(r.flag) + \" (inert), dispute \" +\n\t\tn(r.dispute) + \", author \" + n(r.authorHigh) + \", conviction \" +\n\t\tn(r.conviction) + \", unadjudicated \" + n(r.unadjudicated) +\n\t\t\" bps | The default credit each act earns toward standing. Policing \" +\n\t\t\"ranks above authorship, which ranks above staking. The flag rate is \" +\n\t\t\"still settable and still bounds what authorship may be set to, but no \" +\n\t\t\"path credits it since the quality lane was removed. | \" +\n\t\t\"`SetCreditRatesDefault` |\\n\")\n\tdom := siteDomain\n\tif dom == \"\" {\n\t\tdom = \"_none_\"\n\t}\n\tb.WriteString(\"| Site domain | \" + dom + \" | The website this realm belongs \" +\n\t\t\"to. Also allow-lists that domain for images without listing it below. | \" +\n\t\t\"`SetSiteDomain` |\\n\")\n\thosts := MediaHosts()\n\tif hosts == \"|\" {\n\t\thosts = \"_none_\"\n\t}\n\tb.WriteString(\"| Media hosts | \" + hosts + \" | Where images may be loaded \" +\n\t\t\"from: exact hosts before the bar, suffixes after it. | \" +\n\t\t\"`SetMediaHosts` |\\n\")\n\n\t/* THE EVIDENCE FOR THE ONE CLAIM THIS PAGE MAKES. The creation-burn row\n\t   says the payment \"is burned\", which a reader has no way to check from\n\t   anything else on the page — and a page whose whole job is showing what is\n\t   true should not be the one place making an unverifiable assertion. So the\n\t   destination is named and the running total is shown: both are already\n\t   published (BurnSink, BurnedGNOT) precisely so anyone can reconcile them\n\t   against bank balances.\n\t   BELOW THE TABLE, NOT IN IT. Neither figure is a parameter and nobody can\n\t   set them; putting them in a table of settings, under a \"Set with\" column,\n\t   would misrepresent what they are. */\n\tb.WriteString(\"\\nBurned to date, across every court and every creation: **\" +\n\t\tugnotAmt(BurnedGNOT()) + \" GNOT**, held at `\" + BurnSink().String() +\n\t\t\"` — an address derived from a sub-realm identity that is never created, \" +\n\t\t\"so no key to it exists and nothing can ever be spent from it.\\n\")\n\treturn b.String()\n}\n"},{"name":"answer.gno","body":"// Answers: one per claim, bonded, freezing (PLAN §3.1, pins P6/P13). The\n// answerability gate reads the trailing average of TOTAL STAKE (X̄) — open\n// interest moves only on real stake flows, so a short window is safe (V1's\n// audited argument, unchanged). The answer freezes staking, pins the era\n// integral (H1), and snapshots X̄frozen — the single base every later\n// %-of-X̄ quantity (bonds, slash, flag bond) reads (P6).\n\npackage kourtv2\n\n// answerWindow: the trailing window answerability reads. V1's audited 3-hour\n// value — enough history to block a flash-answer without walling off fresh\n// claims (the price-manipulation concern that forced V1's week-long window is\n// gone: no price exists in V2).\nconst answerWindow = 3 * epochBlocks\n\n// PostAnswer posts THE answer (one per claim, first-come within the rules),\n// escrowing bond = max(6%·X̄frozen, 1.6 × the LARGER side's frozen conviction),\n// min 1, capped by court params if set. The floor is the real sizer; the 6% base\n// is only a dust arm on claims too thin or too young for it to bite.\nfunc PostAnswer(cur realm, courtSlug string, claimID uint64, verdict int) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tmustSide(verdict)\n\tif cs.closed {\n\t\tpanic(\"kourtv2: this claim is closed\")\n\t}\n\tif cs.frozenAt != 0 {\n\t\tpanic(\"kourtv2: this claim already has an answer\")\n\t}\n\n\tnow := heightNow()\n\t// Answerability ENDS at the dead-claim timeout. CloseDeadClaim is\n\t// permissionless but OPTIONAL, so without this a claim stays answerable at any\n\t// age — and every X̄-derived bound in the system (court.gno's maxTcWeeks term,\n\t// the conviction magnitudes, the escrow window) is derived for a ≤12-week\n\t// life. Conviction keeps accruing on an un-closed claim, so past ~19.5 weeks\n\t// (hot) the draw arm outgrows anything the base arm could cover even on a\n\t// perfectly CONSTANT pool, so every %-of-X̄ bound derived for a ≤12-week life\n\t// would be falsified with no drain at all.\n\tif passed, known := pastDeadline(cs.openedAtTime, deadClaimSecs); (known \u0026\u0026 passed) ||\n\t\t(!known \u0026\u0026 now \u003e= cs.openedAt+deadClaimTimeout) {\n\t\tpanic(\"kourtv2: this claim is past the dead-claim timeout; it closes, it is not answered\")\n\t}\n\t// Answerability: mature trailing X̄ at or above the floor.\n\txbar, mature := cs.oi.Average(now, answerWindow)\n\tif !mature {\n\t\tpanic(\"kourtv2: not enough stake history to answer yet\")\n\t}\n\tif xbar \u003c effMinAnswerX(c) {\n\t\tpanic(\"kourtv2: trailing stake is below the minimum to answer\")\n\t}\n\n\t// The priority window (P13, implementation pin): keyed on claim AGE — the\n\t// first priorityWindowBlocks after the earliest the claim could have been\n\t// answerable (openedAt + answerWindow). Deterministic; needs no crossing\n\t// detection; priority is a perk, not a guarantee (a claim that matures\n\t// late may have no priority phase left). Binds only once the court's\n\t// cold-start gate is armed.\n\t// Re-anchored for the polish window (MODERATION.md §9): the earliest\n\t// answerable moment moves by stakeOpenDelayBlocks, because the ring cannot\n\t// mature until staking opens. Without this the priority phase silently\n\t// shrinks — and at the 24h maximum delay it would vanish entirely, since\n\t// the delay cap equals priorityWindowBlocks. Default delay 0 → unchanged.\n\t// The 24h IS 24H NOW, and the sum is what makes that safe to say. The anchor\n\t// is deliberately a PROJECTION from opening — \"the earliest the claim could\n\t// have been answerable\" — not the moment the ring actually matured, which is\n\t// why the note above can call priority a perk rather than a guarantee. A\n\t// projected age converts whole: the two block-denominated terms through\n\t// blocksToSecs, as the polish window already does in stake.gno, and the head\n\t// start as itself.\n\t//\n\t// WHAT THIS GIVES UP, stated because it is a real trade and not a free win:\n\t// counted in blocks the window stretched with cadence, so a slow chain handed\n\t// a late-maturing ring more room to mature inside the window. It no longer\n\t// does. That is the same \"may have no priority phase left\" the design already\n\t// accepts, now reached by a clock instead of by a block rate — and in exchange\n\t// the head start is 24 hours on every chain rather than 24 hours only on one\n\t// that holds 5s a block.\n\t//\n\t// answerWindow appears here as a DURATION, not as the trailing ring it is on\n\t// line 49; that use is accounting and stays in blocks, untouched.\n\tinPriority := false\n\tif passed, known := pastDeadline(cs.openedAtTime,\n\t\tblocksToSecs(c.params.stakeOpenDelayBlocks+answerWindow)+priorityWindowSecs); priorityGateActive(c) \u0026\u0026\n\t\t((known \u0026\u0026 !passed) ||\n\t\t\t(!known \u0026\u0026 now \u003c cs.openedAt+c.params.stakeOpenDelayBlocks+answerWindow+priorityWindowBlocks)) {\n\t\tif !mayAnswerInPriority(c, who) {\n\t\t\tpanic(\"kourtv2: the priority window is open — a qualified answerer may act first (wait ~24h)\")\n\t\t}\n\t\tinPriority = true\n\t}\n\n\t// Settle the pool integrals to this height FIRST: the bond is sized against\n\t// the answered side's conviction, which must therefore be current. (These ran\n\t// just below before; nothing between here and the freeze pin touches pools,\n\t// and a Transfer moves no supply, so the walk is unchanged.)\n\ttouch(c)\n\tadvancePools(c, cs)\n\n\t// P6 BASE: max(3h trailing average, LIFETIME time-averaged total stake).\n\t// The 3h window is the right ANSWERABILITY gate — a liveness question, checked\n\t// above against minAnswerX and deliberately left reading the raw trailing value,\n\t// or a fully drained claim would become unanswerable on history alone. It is the\n\t// WRONG base for SIZING. Every consumer of X̄frozen prices a prize or a\n\t// punishment — this bond, the slash flat arm, the flag bond, the P2 carrot\n\t// clamp, both quality bars, the dispute bond, the quorum floor, the escrow\n\t// window — and each of those guards a quantity set by CONVICTION, a lifetime\n\t// integral. Unstake now forfeits conviction in proportion (stake.gno), which\n\t// removes the prize half of this attack — but NOT the pricing half, which is\n\t// what this paragraph is about and why it still stands: X̄frozen is the base\n\t// for every BAR here, and a drained claim must not become cheap to attack\n\t// merely because it was drained. Historically the two compounded: a pool that\n\t// banked 11 weeks at 100k CC and drained to 100 CC two hours before the answer\n\t// still carried a 100k-CC prize while the 3h arm read 100 CC, mis-pricing every\n\t// consumer by the whole drain factor: a 25 CC demotion bar that lets 25 CC of\n\t// weight force a conclusive LOW and zero a 7_012 CC draw at a PROFIT (the flag\n\t// bond returns whole below the full bar), a 20 CC dispute bond, and a ~1 CC\n\t// carrot clamp against a 5%-of-supply mobilisation. Worse, the drain also\n\t// un-escrows the attacker's weight — escrowed stake is netted out of `votable`\n\t// and barred from the quality lane by isParticipant — so the same act that\n\t// lowers the bars hands the attacker the weight to clear them.\n\t//\n\t// This is also what makes court.gno's deploy check TRUE rather than merely\n\t// calibrated: its identity midGross/X̄ = rateBpsFP·T_c/1e10 is only valid when\n\t// X̄ is the claim's time-averaged stake, since midGross is a lifetime integral.\n\t// Feeding it a 3-hour average was a units mismatch; the drain was its extremal\n\t// exploit. max(), never replace: on any claim whose total stake never decreased\n\t// the trailing arm wins, so this is a provable no-op off the drain path.\n\tif life := lifeAvgStake(cs, now); life \u003e xbar {\n\t\txbar = life\n\t}\n\n\t// The BASE arm, 6%·X̄, never zero; the court's cap applies if set (0 = uncapped).\n\t// This is a floor on the floor: the collateralization floor below is what prices\n\t// an answer on any claim with real conviction behind it.\n\tbond := xbar * answerBondBps / 10000\n\tif c.params.answerBondCapCC \u003e 0 \u0026\u0026 bond \u003e c.params.answerBondCapCC {\n\t\tbond = c.params.answerBondCapCC\n\t}\n\t// THE CONVICTION FLOOR. The 6% base above prices an answer off the claim's\n\t// STAKE; this floor prices it off the claim's CONVICTION, and takes whichever\n\t// is larger. It is applied after the court cap deliberately — the cap must not\n\t// be able to buy a cheap answer on a heavily-converted claim.\n\t//\n\t// IT USED TO BE THE SLASH'S COLLATERAL, and that is no longer why it is here.\n\t// The conclusive-low slash drew 1.6× the answered side's frozen conviction\n\t// while the bond was sized off a 3-hour trailing X̄, so without a floor a mill\n\t// could bank conviction, drain two hours before answering, and post a bond\n\t// 224× smaller than the slash it had earned — measured at ~51 CC against\n\t// 11_220 CC of exposure. The quality lane took the slash with it, and the\n\t// forfeit in stake.gno closed the drain at its source besides.\n\t//\n\t// TWO REASONS IT STAYED ANYWAY, both of which outlive the slash:\n\t//\n\t//   1. The bond tracks the TAKE. An answer on a claim carrying real conviction\n\t//      is worth more to get wrong, and 6%·X̄ does not see conviction at all.\n\t//   2. It is what keeps the DISPUTE bond answerer-independent. That bond is\n\t//      min(20%·X̄, 40%·this). At a 6% base the answer arm binds on every claim,\n\t//      so without the floor a defender's price would depend on which side the\n\t//      attacker chose to declare — measured 1.86× apart. With it they match.\n\t//\n\t// Deleting it with the slash would therefore have re-priced every answer by\n\t// roughly 5× and reopened that spread, neither of which has anything to do\n\t// with quality. The arithmetic is unchanged; only the name and the reason are\n\t// (bondFloorAt, answerbond.gno).\n\t//\n\t// MAX OVER BOTH SIDES, and this is the change that makes a 6% base safe.\n\t// Keyed to the ANSWERED side alone, a dust answer was exempt exactly where it\n\t// did the most damage: stake a token amount on one side, declare it, let 72h\n\t// of silence settle, and the large opposing pool's whole draw is annihilated\n\t// while the bond returns whole — priced at the flat base, because the\n\t// answerer's own conviction is ~0. Measured at 11 weeks, the sniper paid 45 CC\n\t// where the honest majority answerer on the same claim paid 101.\n\t//\n\t// max() is the SAME TAX WITH THE EXEMPTION REMOVED, not a new one. For an\n\t// answerer on the larger pool it is bit-identical (measured: 11 of 14 rows\n\t// unchanged to the unit; the three that move are the dust snipe and the aged\n\t// contrarian). An honest contrarian pays ~6× LESS than at the old 50% base.\n\t//\n\t// STILL LOAD-BEARING ELSEWHERE. OpenDispute's `base \u003c 1` bond floor\n\t// (dispute.gno) is unreachable BECAUSE of this floor: an answered claim's bond\n\t// cannot be small enough for 40% of it to round to zero. Setting\n\t// answerBondCapCC = 1 does NOT produce a bond of 1; measured, it produced\n\t// 18_000_000. If this is ever relaxed, mutate that guard first.\n\tconvHi, convLo := cs.sideConv(verdict)\n\tfloor := bondFloorAt(xbar, convHi, convLo)\n\toppHi, oppLo := cs.sideConv(oppositeSide(verdict))\n\tif opp := bondFloorAt(xbar, oppHi, oppLo); opp \u003e floor {\n\t\tfloor = opp\n\t}\n\tif floor \u003e bond {\n\t\tbond = floor\n\t}\n\tif bond \u003c 1 {\n\t\tbond = 1\n\t}\n\tmustSpendable(c, who, bond)\n\tc.coin.Transfer(who, c.escrow, bond)\n\n\t// FREEZE: conviction is pinned here — immutable by construction (audit H1).\n\tcs.frozenAt = now\n\tcs.rateAccAtFreeze = c.rateAccFP\n\tcs.xBarFrozen = xbar // P6: the one base for every later %-of-X̄ quantity\n\t// THE REFERENCE IS TAKEN BEFORE THE OBSERVATION, so a claim is never\n\t// measured against an average it is itself in. Both land on the claim at the\n\t// same instant, which is what makes the multiplier immutable afterwards.\n\tcs.tierRef = tierRefAt(c)\n\tobserveClaimSize(c, xbar)\n\t// The per-side record, pinned here and never revised: the live counters below\n\t// drain to zero as principal goes home.\n\tcs.yesStakeAtFreeze, cs.noStakeAtFreeze = cs.yesStake, cs.noStake\n\n\tcs.answer = int8(verdict)\n\tcs.answerer = who\n\tcs.answerBond = bond\n\tcs.answerBond0 = bond\n\tcs.answerHeight = now\n\tcs.answeredAtTime = nowTime()\n\tif inPriority {\n\t\tholdPriority(c, who, claimID)\n\t}\n\t// Policing surfaces (§5.3): an answered claim becomes policeable, so it\n\t// enters both strips regardless of any hide bit; a seeded/appeal row that\n\t// was waiting on the pending list moves over. Non-panicking by contract.\n\tpendingExit(c, cs)\n\tstripEnter(c, cs)\n\t// On the meta court: persist the BINDING appeal parse and take the target's\n\t// review slot. Refuses if another appeal already holds it (meta.gno) — the\n\t// one moderation read that gates a money entrypoint, and it reads only the\n\t// latch, never a hide bit (MODERATION.md §2 read gate #4).\n\tonClaimAnswered(c, cs)\n}\n\n// ------------------------------------------------------------------ reads --\n\nfunc HasAnswer(courtSlug string, claimID uint64) bool {\n\treturn mustClaim(mustCourt(courtSlug), claimID).frozenAt != 0\n}\n\nfunc AnswerVerdict(courtSlug string, claimID uint64) int {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tif cs.frozenAt == 0 {\n\t\tpanic(\"kourtv2: no answer yet\")\n\t}\n\treturn int(cs.answer)\n}\n\nfunc AnswerBond(courtSlug string, claimID uint64) int64 {\n\treturn mustClaim(mustCourt(courtSlug), claimID).answerBond\n}\n\nfunc Answerer(courtSlug string, claimID uint64) address {\n\treturn mustClaim(mustCourt(courtSlug), claimID).answerer\n}\n\n// XBarFrozen is the P6 snapshot every bond/slash/flag quantity reads.\nfunc XBarFrozen(courtSlug string, claimID uint64) int64 {\n\treturn mustClaim(mustCourt(courtSlug), claimID).xBarFrozen\n}\n"},{"name":"answerbond.gno","body":"// The answerer's bond, after the quality lane.\n//\n// This file is what survived quality.gno. settleAnswerBond was never about\n// quality — it is the answer bond's terminal disposition, shared by every\n// terminal path that RETURNS the bond — but it lived in that file because the\n// reserve it carved was the flag lane's slash.\npackage kourtv2\n\n// settleAnswerBond returns the answer bond to the answerer at a terminal path.\n//\n// IT USED TO CARVE A SLASH RESERVE FIRST. The flag lane could levy against this\n// bond after settlement, so the bond was split — reserve retained, remainder\n// refunded — and cs.answerBond was left holding the reserve rather than zeroed.\n// With the lane gone there is no later levy to reserve against, and the whole\n// bond is simply the answerer's again. The reserve arithmetic is not kept \"just\n// in case\": a reserve that is provably always zero is dead state that reads\n// like a live risk.\n//\n// ONE COPY BECAUSE THE CODE ASKED FOR ONE. The two terminal paths ran these\n// lines character-identically before this function existed, and the corpus was\n// paying for the copy in the open — M3-HIGH-1 stood as TWO rows with\n// byte-identical find and replace, one aimed at each path.\n//\n// AND IT WAS EXTRACTED WITHOUT BEING ADOPTED. It had two callers — Finalize and\n// SettleUndisputed — while THREE more terminal paths still ran the two lines\n// inline: open the rewards (the mid happy path), spamDiscardClaim and provCloseClaim.\n// So the copy this function exists to remove had gone from two to three\n// somewhere else, which is the failure mode of extracting a helper and leaving\n// the call sites alone: the next change to the disposition reaches two paths of\n// five and nothing says so. All five now call it.\n//\n// FIVE REFUND PATHS, NOT SIX. dispute.gno's overturn arm also zeroes\n// cs.answerBond, and it must NOT come here: that path BURNS the bond as the\n// forfeiture, and routing a burn through a function whose one job is to hand the\n// coin back is how a forfeiture quietly becomes a refund.\n//\n// The caller keeps its own `if cs.answerBond \u003e 0` guard: each terminal path has\n// its own reason to be standing there and its own note about it, and what is\n// shared is only what happens once it is.\nfunc settleAnswerBond(c *Court, cs *claimState) {\n\tc.coin.Transfer(c.escrow, cs.answerer, cs.answerBond)\n\tcs.answerBond = 0\n}\n\n// sideConv is one side's pool conviction accumulator (u128). It is read from\n// accumulators that freeze at the answer (advancePools settles them in\n// PostAnswer; stake and unstake refuse after), so the value is identical at\n// PostAnswer, at settle and at open the rewards — which is what lets the bond floor\n// and the draw share one reading of the same quantity.\nfunc (cs *claimState) sideConv(side int) (hi, lo uint64) {\n\tif side == sideYES {\n\t\treturn cs.yesConvHi, cs.yesConvLo\n\t}\n\treturn cs.noConvHi, cs.noConvLo\n}\n\n// bondFloorAt sizes the answer bond's conviction floor: max(4.5%·X̄, 1.6× the\n// mid draw a side's conviction would take). Parameterized on the base and one\n// side's conviction so PostAnswer can price it before the freeze pins land.\n//\n// THIS WAS slashSizeAt AND IT SIZED A PENALTY. The conclusive-low slash was\n// max(4.5%·X̄, 1.6× the mill's would-be mid draw), and the answer bond was\n// floored at it so a levy could never clamp from under its own collateral —\n// one function with two callers, so \"the bond collateralizes the slash\" held by\n// SHARED CODE rather than by two arithmetic paths a drained pool could pull\n// apart. The slash is gone with the quality lane. The floor is not, and\n// answer.gno says at the call site what it is still for.\nfunc bondFloorAt(xbar int64, convHi, convLo uint64) int64 {\n\tflat := xbar * bondFloorFlatBps / 10000\n\tif draw := mulDiv128(convToCC(convHi, convLo), bondFloorConvBps, 10000); draw \u003e flat {\n\t\treturn draw\n\t}\n\treturn flat\n}\n\n// bondFloorFor is the floor keyed to the ANSWERED side, which is what PostAnswer\n// prices before it takes the max over both sides. Kept as a named function\n// because the tests that pin the bond compute the same quantity, and two\n// arithmetic paths for one number is how they drift apart.\nfunc bondFloorFor(cs *claimState) int64 {\n\thi, lo := cs.sideConv(int(cs.answer))\n\treturn bondFloorAt(cs.xBarFrozen, hi, lo)\n}\n"},{"name":"association.gno","body":"// Associations: claims that bear on other claims (COURTS_STRUCTURE.md §5, and\n// docs/ASSOCIATIONS.md for this design and what it deliberately leaves out).\n//\n// ONE THING, TWO NAMES, and this is the only place that has to say so. §5 calls\n// it an ARGUMENT edge, and every quotation of §5 in this tree keeps that word,\n// because a citation that paraphrases is not a citation. The code calls it an\n// ASSOCIATION: \"argument\" is already the word for a function's parameters, and a\n// file with both meanings in it is a file where `TestOrderFoldersBoundsIts-\n// Argument` reads like it is about this feature. So: §5's argument edge is this\n// realm's association, in the same way §5's \"counters\" is this realm's\n// `assocContests`.\n//\n// §5's table is the whole specification and it is short: an association has a\n// stance (supports / counters), any number may exist, ANYBODY may add one at any\n// time, and it does nothing mechanical. That last line is load-bearing —\n//\n//\t\"a settled supporting claim does not move its parent's odds, does not lower\n//\t a bar, does not force a verdict... Views may aggregate; the chain does not\n//\t infer.\"\n//\n// — so no money path reads any of this. It is render-layer state, the same\n// posture stakeseries.gno takes about itself.\n//\n// NOT IMPLEMENTED HERE, and both are findings rather than omissions. The overlay\n// draws three edge kinds and says they match §5; two of them do not.\n// CONTAINMENT (`part-of`) is §5's other edge, but its own guards say \"only\n// sections may be containment parents\" and §6 defers sections — so claim→claim\n// containment is the shape §5 forbids, and flat folders are the containment this\n// realm actually chose. SUPERSEDES has no §5 entry; it means \"a re-filing after a\n// claim died unanswered\", which this docket produces constantly. It is designed\n// in docs/SUPERSEDES.md and deliberately not built here, because it is not the\n// same kind of thing: an association is an opinion and this is a checkable fact\n// about the docket (`claimState.closed` is the realm's own record that a claim\n// died without an answer), so it wants a verifying entrypoint rather than a\n// second copy of this one.\n\npackage kourtv2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\nconst (\n\tassocSupports = int8(0)\n\tassocContests = int8(1)\n\t// THE CAPS BOUND A READ, not a writer's enthusiasm. A claim page lists every\n\t// incident edge, so without a cap its render grows with whatever an attacker\n\t// is willing to pay to write — the failure seriesRowCap exists to prevent\n\t// elsewhere in this realm.\n\tmaxAssocOut = 32\n\tmaxAssocIn  = 64\n\t// The anti-sybil term, and the reason the inbound cap is not itself a free\n\t// griefing vector: filling one claim's 64 inbound slots takes 16 distinct\n\t// addresses, each paying its own storage deposit. That is the same price\n\t// every other flood surface here pays.\n\tmaxAssocInPerAuthor = 4\n)\n\n// ---- the association bond ------------------------------------------------\n//\n// WHY A BOND, AND ONLY FOR STRANGERS. COURTS_STRUCTURE.md §5 opens this edge to\n// \"anybody, any time, no mechanical effect\", and the note above says why that is\n// not already a free griefing vector: filling one claim's inbound slots takes\n// sixteen addresses, each paying its own storage deposit. The judgement here is\n// that a storage deposit is too cheap to deter somebody who WANTS those slots\n// full, and too blunt to tell a stranger's counter-evidence from a flood.\n//\n// THE REFUND IS THE POINT, and it is what keeps this on the right side of §5. A\n// bond that is never returned is a price on speech — exactly what \"no mechanical\n// effect\" was protecting, and what AddAssociation's own note calls pricing out the\n// counter-evidence this edge exists to carry. A bond returned once a moderator\n// agrees the edge was worth attaching costs an honest contributor nothing but\n// the wait, and costs a griefer everything.\n//\n// Nobody accountable pays it: the asserting claim's own author has already put a\n// deposited claim behind their name, and a moderator is answerable by election.\n//\n// TWO PLACES, deliberately. The realm admin sets the default so a new court is\n// never unpriced; a court's own moderators may override it, because a small\n// trusted docket should not need a realm upgrade to make association free, and a\n// court under attack should not have to wait for one to make it dear.\nconst assocBondDefaultInit = int64(1_000_000) // 1 CC, the carrotClampMinCC scale\n\nvar assocBondDefault = assocBondDefaultInit\n\n// assocBondFor resolves the two places: this court's override, else the realm\n// default. Court.assocBond says why zero there means unset rather than free.\nfunc assocBondFor(c *Court) int64 {\n\tif c.assocBond \u003e 0 {\n\t\treturn c.assocBond\n\t}\n\treturn assocBondDefault\n}\n\n// SetAssociationBondDefault sets the realm-wide default every court inherits.\nfunc SetAssociationBondDefault(cur realm, amount int64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\td := ensureGlobalDAO()\n\tif cur.Previous().Address() != d.admin {\n\t\tpanic(\"kourtv2: only the global DAO admin sets the default association bond\")\n\t}\n\tif amount \u003c 0 {\n\t\tpanic(\"kourtv2: an association bond cannot be negative\")\n\t}\n\tassocBondDefault = amount\n}\n\n// SetCourtAssociationBond overrides the default for one court; 0 clears the\n// override and returns the court to the realm default.\nfunc SetCourtAssociationBond(cur realm, courtSlug string, amount int64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\t// An ACTIVE moderator: repricing association is not one of the de-escalatory\n\t// acts a suspended set may still take.\n\trequireActiveMod(ensureMod(c), who)\n\tif amount \u003c 0 {\n\t\tpanic(\"kourtv2: an association bond cannot be negative\")\n\t}\n\tc.assocBond = amount\n}\n\n// AssociationBond is what a stranger must post to attach an edge in this court.\nfunc AssociationBond(courtSlug string) int64 { return assocBondFor(mustCourt(courtSlug)) }\n\n// AssociationBondDefault is the realm-wide default, for a page that wants to say\n// which of the two places a court's figure came from.\nfunc AssociationBondDefault() int64 { return assocBondDefault }\n\n// assocBondWindowSecs is how long a bond waits on a moderator before its author\n// may take it back.\n//\n// UNJUDGED MEANS APPROVED, and the direction is the whole design. The other way\n// round — burn what nobody judged — makes moderator inactivity a confiscation\n// engine, and a court with one absent moderator would quietly keep every\n// stranger's CC. Two weeks is long enough for a moderator who is paying any\n// attention and short enough that an honest contributor is not out of pocket for\n// a quarter waiting on one who is not.\nconst assocBondWindowSecs = int64(14 * 86400)\n\n// mustAssocEdge is the lookup every association verb starts with. Both the nil\n// tree and the missing key answer the caller the same way, because the\n// difference between \"this court has no edges\" and \"this pair has none\" is not\n// a distinction the caller can act on.\nfunc mustAssocEdge(c *Court, from, to uint64) *assocEdge {\n\tif c.assocOut == nil {\n\t\tpanic(\"kourtv2: no such association\")\n\t}\n\tv := c.assocOut.Get(assocKey(from, to))\n\tif v == nil {\n\t\tpanic(\"kourtv2: no such association\")\n\t}\n\treturn v.(*assocEdge)\n}\n\n// dropAssocEdge clears BOTH indexes, or the edge survives in one direction and the\n// two reads disagree about whether it exists.\nfunc dropAssocEdge(c *Court, from, to uint64) {\n\tc.assocOut.Remove(assocKey(from, to))\n\tc.assocIn.Remove(assocKey(to, from))\n}\n\n// burnAssocBond destroys a held bond and zeroes it, so nothing can be burned\n// twice. The exact mirror of releaseAssocBond, and deliberately shaped like it:\n// both move the money and neither touches the edge.\n//\n// THREE CALLERS AND IT DOES NOT DROP THE EDGE, because they disagree about\n// dropping and only about that: DisapproveAssociation and the bulk burn drop as\n// part of the judgement, while RemoveAssociation drops every edge it is given,\n// bonded or not, so a drop in here would be a second one. Writing the burn out\n// three times was the first shape, and check-mutation-anchors refused it — two\n// corpus rows had become ambiguous across the copies. That is the guard doing\n// its job: a second spelling of one rule is how one copy comes to forget to\n// zero the field, and an unzeroed bond can be burned again.\nfunc burnAssocBond(c *Court, e *assocEdge) {\n\tamt := e.bond\n\te.bond = 0\n\tc.coin.Burn(c.escrow, amt)\n}\n\n// releaseAssocBond returns a held bond to the edge's author and zeroes it, so\n// nothing can be released twice.\nfunc releaseAssocBond(c *Court, e *assocEdge) {\n\tif e.bond \u003c= 0 {\n\t\treturn\n\t}\n\tamt := e.bond\n\te.bond = 0\n\tc.coin.Transfer(c.escrow, e.author, amt)\n}\n\n// ApproveAssociation returns a stranger's bond: a moderator has judged the edge\n// worth carrying. The edge stays either way — approval is about the money, not\n// about the assertion, which anybody may make (§5).\nfunc ApproveAssociation(cur realm, courtSlug string, from, to uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\trequireActiveMod(ensureMod(c), who)\n\te := mustAssocEdge(c, from, to)\n\tif e.bond \u003c= 0 {\n\t\tpanic(\"kourtv2: no bond is held on that edge\")\n\t}\n\treleaseAssocBond(c, e)\n}\n\n// DisapproveAssociation burns the bond and drops the edge.\n//\n// SINGLE-SIGNER, matching removal: a moderator can already delete any edge\n// outright, so the judgement this adds is over money the griefer chose to put at\n// risk, not over the record. The MASS form is the one that takes m-of-n.\nfunc DisapproveAssociation(cur realm, courtSlug string, from, to uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\trequireActiveMod(ensureMod(c), who)\n\te := mustAssocEdge(c, from, to)\n\tif e.bond \u003c= 0 {\n\t\tpanic(\"kourtv2: no bond is held on that edge; remove it instead\")\n\t}\n\tburnAssocBond(c, e)\n\tdropAssocEdge(c, from, to)\n}\n\n// ClaimAssociationBond lets the author take back a bond no moderator judged\n// inside the window. The edge stays; only the money moves.\nfunc ClaimAssociationBond(cur realm, courtSlug string, from, to uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\te := mustAssocEdge(c, from, to)\n\tif e.bond \u003c= 0 {\n\t\tpanic(\"kourtv2: no bond is held on that edge\")\n\t}\n\tif e.author != who {\n\t\tpanic(\"kourtv2: only the edge's author may take its bond back\")\n\t}\n\tif nowTime() \u003c e.bondUntil {\n\t\tpanic(\"kourtv2: the moderation window on that bond has not closed yet\")\n\t}\n\treleaseAssocBond(c, e)\n}\n\n// pendingIncident collects every edge incident to one claim that still holds a\n// bond, as two lists of the OTHER endpoint: outs are the `to` of edges this claim\n// asserts, ins are the `from` of edges asserted about it.\n//\n// BOTH DIRECTIONS, because both caps are griefing vectors. A stranger may fill a\n// claim's 32 outbound slots as readily as its 64 inbound ones — AddAssociation's\n// `from` is any claim in the court, not the caller's own — so \"clear the junk off\n// this claim\" is not true if it clears half of it.\n//\n// COLLECTED BEFORE ANYTHING IS SETTLED, and that is not tidiness: the disapprove\n// path REMOVES keys from the very trees this walks, and mutating a bptree\n// mid-iteration is undefined. The two slices are bounded by maxAssocOut and\n// maxAssocIn, which is what makes the mass verbs cost no more than the caps the\n// write path already enforces.\nfunc pendingIncident(c *Court, claimID uint64) ([]uint64, []uint64) {\n\touts, ins := []uint64{}, []uint64{}\n\tif c.assocOut == nil {\n\t\treturn outs, ins\n\t}\n\tp := beClaimKey(claimID)\n\tc.assocOut.Iterate(p, \"\", func(key string, v any) bool {\n\t\tif !strings.HasPrefix(key, p) {\n\t\t\treturn true\n\t\t}\n\t\tif e := v.(*assocEdge); e.bond \u003e 0 {\n\t\t\touts = append(outs, claimIDFromKey(key[8:]))\n\t\t}\n\t\treturn false\n\t})\n\tc.assocIn.Iterate(p, \"\", func(key string, v any) bool {\n\t\tif !strings.HasPrefix(key, p) {\n\t\t\treturn true\n\t\t}\n\t\tif e := v.(*assocEdge); e.bond \u003e 0 {\n\t\t\tins = append(ins, claimIDFromKey(key[8:]))\n\t\t}\n\t\treturn false\n\t})\n\treturn outs, ins\n}\n\n// ApproveAllAssociations settles every bond held on an edge incident to one\n// claim, in one transaction: a moderator has read the claim's page and judged\n// what is on it worth carrying. Returns how many bonds it returned.\n//\n// SINGLE-SIGNER, like the individual approve, and the reason does not carry over\n// to its mirror: this verb only ever gives money BACK. A captured moderator using\n// it refunds griefers, which costs the court nothing it ever had — the bonds were\n// never the court's — and takes away no other remedy: the edges can still be\n// removed, hidden, or struck one at a time.\n//\n// PER CLAIM, not per court, and that is what makes it bounded. The griefing this\n// answers IS per claim — ASSOCIATIONS.md names the inbound cap itself as the\n// vector — and a claim's incident edges are bounded by maxAssocOut+maxAssocIn exactly.\n// A per-court sweep would cost O(edges in the docket): a transaction that gets\n// more expensive as the court succeeds and one day cannot be sent at all. Court\n// scope would need a third index holding only bonded edges; that is the design if\n// it is ever wanted, and it is not free, so it waits for a court that needs it.\nfunc ApproveAllAssociations(cur realm, courtSlug string, claimID uint64) int {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\trequireActiveMod(ensureMod(c), who)\n\tmustClaim(c, claimID)\n\touts, ins := pendingIncident(c, claimID)\n\tn := 0\n\tfor _, to := range outs {\n\t\treleaseAssocBond(c, mustAssocEdge(c, claimID, to))\n\t\tn++\n\t}\n\tfor _, from := range ins {\n\t\treleaseAssocBond(c, mustAssocEdge(c, from, claimID))\n\t\tn++\n\t}\n\treturn n\n}\n\n// DisapproveAllAssociations judges every bonded association edge touching one\n// claim at once: the bonds are BURNED and the edges dropped. Returns how many\n// were burned, and 0 while the threshold is still short.\n//\n// reason is recorded on the moderation log and is required.\n//\n// SEVERAL MODERATORS MUST AGREE, at the court's moderation threshold — unlike\n// approving in bulk and unlike disapproving one edge, both of which are\n// single-signer. The difference is how much of other people's money one\n// signature may destroy: a claim can carry up to 96 incident edges, and emptying\n// them all is most of a page's contested state.\n//\n// ONLY EDGES THAT PREDATE THE PROPOSAL ARE BURNED. A bond posted between the\n// first signature and the one that fires it has been judged by nobody, so it\n// survives and its edge stays.\n//\n// Refused when the court or claim does not exist, the caller is not an active\n// moderator, or the reason is missing or too long.\nfunc DisapproveAllAssociations(cur realm, courtSlug string, claimID uint64, reason string) int {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\trequireActiveMod(cm, who)\n\tcheckReason(reason)\n\tmustClaim(c, claimID)\n\n\tkey := \"assocburn:\" + strconv.FormatUint(claimID, 10)\n\t// Read BEFORE this signature joins, or a set of one always reads its own\n\t// height and the bound means nothing.\n\topened := pendingOpenedAt(cm.pending, key)\n\tfire, _ := approveAction(cm.pending, key, who, reason, cm.m)\n\tif !fire {\n\t\treturn 0\n\t}\n\tif opened == 0 {\n\t\topened = heightNow() // this call both opened and fired it\n\t}\n\n\touts, ins := pendingIncident(c, claimID)\n\tn := 0\n\tfor _, to := range outs {\n\t\tn += burnIfOlder(c, claimID, to, opened)\n\t}\n\tfor _, from := range ins {\n\t\tn += burnIfOlder(c, from, claimID, opened)\n\t}\n\treturn n\n}\n\n// burnIfOlder burns one edge's bond and drops the edge, but only if the edge was\n// written at or before `opened`. Returns 1 if it did.\nfunc burnIfOlder(c *Court, from, to uint64, opened int64) int {\n\te := mustAssocEdge(c, from, to)\n\tif e.bond \u003c= 0 || e.at \u003e opened {\n\t\treturn 0\n\t}\n\tburnAssocBond(c, e)\n\tdropAssocEdge(c, from, to)\n\treturn 1\n}\n\n// assocEdge is ONE object held in TWO indexes. assocOut and assocIn store the same\n// pointer, so a stance cannot desync from itself and removal has to clear both\n// or the edge survives in one direction — which is what the removal test pins.\ntype assocEdge struct {\n\t// bond is the CC this edge's author posted and has not got back: held in\n\t// c.escrow while no moderator has judged it, and zeroed the moment it moves\n\t// either way. So `bond \u003e 0` IS the pending state and there is no second\n\t// flag to disagree with it — a refund or a burn cannot happen twice because\n\t// the second attempt finds nothing to move.\n\tbond int64\n\t// bondUntil is when the author may take an unjudged bond back, in UNIX\n\t// SECONDS rather than blocks. clock.gno's ruling: a deadline a user plans\n\t// around gates on block time, because a height only means a date if the\n\t// chain's pace never changes. Bonds only exist on edges written after this\n\t// field did, so unlike the claim deadlines there is no height fallback to\n\t// keep — every bonded edge has a real stamp.\n\tbondUntil int64\n\n\tstance int8\n\tauthor address\n\t// at is the height the edge was written. This said \"for the record; nothing\n\t// gates on it\", and DisapproveAllAssociations changed that: the bulk burn\n\t// settles only edges at or below the height its m-of-n proposal opened, so\n\t// this field is what keeps a bond posted mid-vote out of a burn nobody\n\t// consented to. Height and not time, deliberately — it is compared against\n\t// approval.openedAt, which is a height, and the two must be the same clock.\n\tat int64\n}\n\n// ensureAssocs allocates the two indexes. WRITE PATHS ONLY — it is named in\n// check-read-purity.py's ALLOCATORS for that reason. A read that reached for it\n// would let the first caller change what the second one sees, and a query\n// carries no storage deposit, so it would be unpaid state growth.\nfunc ensureAssocs(c *Court) {\n\tif c.assocOut == nil {\n\t\tc.assocOut = bptree.NewBPTree32()\n\t}\n\tif c.assocIn == nil {\n\t\tc.assocIn = bptree.NewBPTree32()\n\t}\n}\n\n// assocKey is the 16-byte composite both indexes use, so \"every edge incident to\n// claim X\" is one prefix scan rather than a walk of the whole tree.\nfunc assocKey(a, b uint64) string { return beClaimKey(a) + beClaimKey(b) }\n\n// assocTextGone reports whether a claim's text is withheld — a purged or globally\n// redacted claim. An edge pointing at one is not returned, for the same reason\n// its title is not.\nfunc assocTextGone(c *Court, id uint64) bool {\n\tif c.mod == nil {\n\t\treturn false\n\t}\n\tclm := lookupClaimMod(c.mod, id)\n\treturn clm != nil \u0026\u0026 (clm.purged || clm.global)\n}\n\n// AddAssociation records that claim `from` bears on claim `to`, with a stance.\n// stance is \"supports\" or \"contests\". Both claims must be in the named court.\n//\n// ANYBODY MAY ASSERT AN EDGE. Most callers post a REFUNDABLE BOND first, held in\n// the court escrow: it comes back in full if a moderator approves the edge, or\n// by default if none looks at it inside the bond window. It is burned only if a\n// moderator disapproves. Two callers pay nothing — the author of `from`, who\n// already has a deposit behind their name, and an active court moderator.\n// AssociationBond(courtSlug) quotes the amount.\n//\n// The price falls on being WRONG, not on speaking.\n//\n// Refused when the court does not exist, `from` equals `to`, either claim does\n// not exist in that court, the stance is not one of the two words, that exact\n// edge already exists (remove it to change its stance), `from` already asserts\n// 32 associations, `to` already carries 64, the caller already holds 4 on `to`,\n// or the caller's DisposableOf is short of the bond. Nothing is charged until\n// every one of those has passed.\nfunc AddAssociation(cur realm, courtSlug string, from, to uint64, stance string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tif from == to {\n\t\tpanic(\"kourtv2: a claim cannot bear on itself\")\n\t}\n\tvar st int8\n\tswitch stance {\n\tcase \"supports\":\n\t\tst = assocSupports\n\tcase \"contests\":\n\t\tst = assocContests\n\tdefault:\n\t\tpanic(\"kourtv2: a stance is \\\"supports\\\" or \\\"contests\\\"\")\n\t}\n\t// Both endpoints must exist IN THIS COURT. Cross-court edges would put a\n\t// second court's id in the key and split one object's moderation authority\n\t// between two moderator sets.\n\tf := mustClaim(c, from)\n\tmustClaim(c, to)\n\n\t// WHO DOES NOT PAY. The asserting claim's own author already has a deposited\n\t// claim behind their name, and an active moderator is answerable by election.\n\t// Everybody else posts a refundable bond — see the note above the constant.\n\t// Resolved here but SPENT below, after every refusal has had its chance: a\n\t// call that is going to panic must not have moved money first.\n\tbond := int64(0)\n\tif f.author != who \u0026\u0026 !isActiveMod(ensureMod(c), who) {\n\t\tbond = assocBondFor(c)\n\t}\n\n\tensureAssocs(c)\n\tk := assocKey(from, to)\n\tif c.assocOut.Get(k) != nil {\n\t\tpanic(\"kourtv2: that edge already exists; remove it to change its stance\")\n\t}\n\n\t// One scan answers all three caps: how many this claim already asserts, how\n\t// many are already asserted about the target, and how many of those are this\n\t// author's.\n\toutN := 0\n\tc.assocOut.Iterate(beClaimKey(from), \"\", func(key string, _ any) bool {\n\t\tif !strings.HasPrefix(key, beClaimKey(from)) {\n\t\t\treturn true\n\t\t}\n\t\toutN++\n\t\treturn outN \u003e maxAssocOut\n\t})\n\tif outN \u003e= maxAssocOut {\n\t\tpanic(\"kourtv2: this claim already asserts \" + strconv.Itoa(maxAssocOut) + \" associations\")\n\t}\n\tinN, mine := 0, 0\n\tc.assocIn.Iterate(beClaimKey(to), \"\", func(key string, v any) bool {\n\t\tif !strings.HasPrefix(key, beClaimKey(to)) {\n\t\t\treturn true\n\t\t}\n\t\tinN++\n\t\tif e, ok := v.(*assocEdge); ok \u0026\u0026 e.author == who {\n\t\t\tmine++\n\t\t}\n\t\treturn false\n\t})\n\tif inN \u003e= maxAssocIn {\n\t\tpanic(\"kourtv2: that claim already carries \" + strconv.Itoa(maxAssocIn) + \" associations\")\n\t}\n\tif mine \u003e= maxAssocInPerAuthor {\n\t\tpanic(\"kourtv2: you already hold \" + strconv.Itoa(maxAssocInPerAuthor) +\n\t\t\t\" associations on that claim\")\n\t}\n\n\t// Every refusal above has fired, so this is the first line that can cost the\n\t// caller anything.\n\tif bond \u003e 0 {\n\t\tmustSpendable(c, who, bond)\n\t\tc.coin.Transfer(who, c.escrow, bond)\n\t}\n\te := \u0026assocEdge{stance: st, author: who, at: heightNow(), bond: bond}\n\tif bond \u003e 0 {\n\t\te.bondUntil = nowTime() + assocBondWindowSecs\n\t}\n\tc.assocOut.Set(k, e)\n\tc.assocIn.Set(assocKey(to, from), e)\n}\n\n// RemoveAssociation withdraws an edge. The edge's OWN AUTHOR, always; an active\n// court moderator, always. Not the target claim's author — a claim's author\n// deleting the edges that say \"this is contested by #12\" is exactly the\n// censorship an association graph exists to resist, and it is the one removal rule\n// worth being unable to bend.\nfunc RemoveAssociation(cur realm, courtSlug string, from, to uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tif c.assocOut == nil {\n\t\tpanic(\"kourtv2: no such association\")\n\t}\n\tv := c.assocOut.Get(assocKey(from, to))\n\tif v == nil {\n\t\tpanic(\"kourtv2: no such association\")\n\t}\n\te := v.(*assocEdge)\n\trequireEdgeRemover(c, e.author, who)\n\t// A HELD BOND FOLLOWS WHOEVER REMOVED THE EDGE. An author withdrawing their\n\t// own assertion takes their bond with them — they are not being judged. A\n\t// moderator removing it HAS judged it, which is the same act as\n\t// DisapproveAssociation, so it burns; otherwise removal would be the cheap\n\t// way around the burn and the bond would deter nothing.\n\tif e.bond \u003e 0 {\n\t\tif e.author == who {\n\t\t\treleaseAssocBond(c, e)\n\t\t} else {\n\t\t\tburnAssocBond(c, e)\n\t\t}\n\t}\n\tdropAssocEdge(c, from, to)\n}\n\n// ClaimAssociations is every edge incident to one claim, packed:\n//\n//\tout:\u003cto\u003e:\u003cs|c\u003e,\u003cto\u003e:\u003cs|c\u003e;in:\u003cfrom\u003e:\u003cs|c\u003e,...\n//\n// Both halves are always present and either may be empty (\"out:;in:\"). Packed\n// rather than returned as a struct for the same reason ClaimSeries is: one read,\n// one round trip, and a shape the overlay already knows how to parse.\n//\n// Bounded by construction — maxAssocOut + maxAssocIn rows, whatever anybody writes.\n// Reads nil trees as empty and never allocates: a court that has never carried an\n// edge answers \"out:;in:\" without the query creating anything.\nfunc ClaimAssociations(courtSlug string, claimID uint64) string {\n\tc := mustCourt(courtSlug)\n\tmustClaim(c, claimID)\n\tvar b strings.Builder\n\tb.WriteString(\"out:\")\n\tif c.assocOut != nil {\n\t\tp := beClaimKey(claimID)\n\t\tn := 0\n\t\tc.assocOut.Iterate(p, \"\", func(key string, v any) bool {\n\t\t\tif !strings.HasPrefix(key, p) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\te := v.(*assocEdge)\n\t\t\tother := claimIDFromKey(key[8:])\n\t\t\tif assocTextGone(c, other) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif n \u003e 0 {\n\t\t\t\tb.WriteString(\",\")\n\t\t\t}\n\t\t\tb.WriteString(strconv.FormatUint(other, 10) + \":\" + stanceCode(e.stance))\n\t\t\tn++\n\t\t\treturn false\n\t\t})\n\t}\n\tb.WriteString(\";in:\")\n\tif c.assocIn != nil {\n\t\tp := beClaimKey(claimID)\n\t\tn := 0\n\t\tc.assocIn.Iterate(p, \"\", func(key string, v any) bool {\n\t\t\tif !strings.HasPrefix(key, p) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\te := v.(*assocEdge)\n\t\t\tother := claimIDFromKey(key[8:])\n\t\t\tif assocTextGone(c, other) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif n \u003e 0 {\n\t\t\t\tb.WriteString(\",\")\n\t\t\t}\n\t\t\tb.WriteString(strconv.FormatUint(other, 10) + \":\" + stanceCode(e.stance))\n\t\t\tn++\n\t\t\treturn false\n\t\t})\n\t}\n\treturn b.String()\n}\n\nfunc stanceCode(s int8) string {\n\tif s == assocContests {\n\t\treturn \"c\"\n\t}\n\treturn \"s\"\n}\n\n// claimIDFromKey is beClaimKey's inverse, for reading the other endpoint back\n// out of a composite key.\nfunc claimIDFromKey(k string) uint64 {\n\tif len(k) \u003c 8 {\n\t\treturn 0\n\t}\n\tvar id uint64\n\tfor i := 0; i \u003c 8; i++ {\n\t\tid = id\u003c\u003c8 | uint64(k[i])\n\t}\n\treturn id\n}\n"},{"name":"board.gno","body":"// The board: a claim's comments (docs/CLAIM_BOARDS.md §6). posting.gno owns who\n// may write and how fast; this file owns what they wrote.\n//\n// DEPTH ONE. A flat list of top-level comments, each carrying its direct\n// replies, and no deeper. Two reasons, and the first is not aesthetic:\n// pagination stops being definable on a tree — you cannot say \"25 per page\" when\n// one subtree is 200 rows — and a flooder can push things around by replying\n// deep rather than by posting more. The second is that an evidence record wants\n// to be readable in order.\n//\n// ROWS ARE IMMUTABLE. No edit, because a text mutation outside purge breaks I4\n// and because a revisable comment lets its author answer a reply and then rewrite\n// what the reply was answering. No author delete, because that is\n// delete-the-evidence-after-the-quote. An author may HIDE their own row, which is\n// a discovery bit and leaves the row and its id in place.\n//\n// FOUR INDEXES PER CLAIM, and each earns its place:\n//\n//\tboard     be(rowID)           -\u003e *boardRow   every row, addressable by id\n//\tboardTop  be(rowID)           -\u003e *boardRow   top-level only, for pagination\n//\tboardKids be(parent)|be(rowID)-\u003e *boardRow   one prefix scan per parent\n//\tboardWrote string(addr)       -\u003e int64       rows per author, for the cap\n//\n// THE INDEXES HOLD THE ROW, NOT A PLACEHOLDER. They used to store `true` and\n// every reader then did a second descent into `board` to fetch what the index\n// had just walked past — six redundant tree descents on a page render. A pointer\n// is the same width as the bool it replaces and there is one object either way,\n// so the second lookup bought nothing. `board` remains the addressable-by-id\n// store, which is what mustBoardRow and the deep link need.\n//\n// boardTop exists because bptree's offset iteration takes no start or end bound\n// (tree.gno: IterateByOffset/ReverseIterateByOffset walk the WHOLE tree), so a\n// prefix of a shared tree cannot be offset-paged. Newest-first over a tree that\n// holds exactly the top-level rows is one reverse walk; the same query against a\n// prefix of `board` would be O(everything before it).\n//\n// boardWrote is a counter rather than a scan for the reason stakeindex.gno gives\n// about its own index: the alternative is O(rows) on every write, and \"measured,\n// that tax is ~57 bytes on a write that already costs ~604k gas\" is the trade\n// this realm has already made once and found worth making.\n\npackage kourtv2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\tbptree \"gno.land/p/nt/bptree/v0\"\n\tsanitize \"gno.land/p/nt/markdown/sanitize/v0\"\n)\n\nconst (\n\t// A couple of paragraphs. The cap is a legal surface as much as a render\n\t// budget: a short-post board is a materially smaller defamation surface than\n\t// a long-form one, and it bounds what one row can cost a page.\n\tmaxBoardTextLen = 2000\n\n\t// Per claim, across everyone. The ceiling on what one claim's board can cost\n\t// to serve, whatever anybody is willing to pay to write.\n\tmaxBoardRowsPerClaim = 512\n\n\t// Per claim, per author — a PAGE-COMPOSITION rule and not a rate limit, which\n\t// posting.gno's throttle already is. Flat across levels on purpose: the grief\n\t// is concentration on one claim, and a top-rung poster has no more right to\n\t// fill a single page than an entry-rung one.\n\tmaxBoardRowsPerAuthor = 8\n\n\t// How many direct replies a top-level row renders. Depth one means a reply\n\t// cannot itself be replied to, so this bounds the whole subtree.\n\tmaxBoardRepliesShown = 8\n)\n\n// boardRow is one comment. `hidden` is a DISCOVERY bit — the row keeps its id,\n// its author and its place in the ordering, and its own route still serves it\n// (I3). Only purge removes text.\ntype boardRow struct {\n\tid     uint64\n\tparent uint64 // 0 = top-level; otherwise the top-level row this answers\n\tauthor address\n\ttext   string\n\t// score is the sum of its upvoters' standing, and scoreKey is the index key\n\t// that CURRENTLY holds it. Storing the key is what lets a re-key remove the\n\t// old entry rather than a recomputed one that may no longer match.\n\tscore    int64\n\tscoreKey string\n\t// at is the height the comment landed, and it is RENDERED on the row's own\n\t// page — a deep link is a citation handle, and a citation without a date is\n\t// half a citation on a claim whose market moves under it. Nothing gates on\n\t// it. A parallel atTime in unix seconds was carried here too and read by\n\t// nothing; height is what every other record in this realm cites by, so the\n\t// second clock was a duplicate with no consumer.\n\tat int64\n\t// TWO HIDE BITS, TWO AUTHORITIES. They were one field, and that made\n\t// HideOwnComment a one-call reversal of a speechM-of-n moderator act: the\n\t// sanctioned author called it with hide=false and the row re-entered the\n\t// ranked index. Symmetrically, UnhideBoardRow exposed a row its author had\n\t// withdrawn. This is the same argument the legal bits below already carry —\n\t// different acts by different authorities, so clearing one must not clear the\n\t// other — applied one step further than it was.\n\t//\n\t// Both are discovery-only: the deep link serves either way. boardMark reads\n\t// their disjunction, so the wire and the index see one \"hidden\" state and only\n\t// the render says WHICH, because \"withdrawn by its author\" is false of a row\n\t// a moderator removed and is an assertion about a person either way.\n\thiddenByAuthor bool\n\thiddenByMod    bool\n\n\t// The legal bits (boardlegal.gno). ON THE ROW rather than in a side tree,\n\t// because the two text gates below are the whole withholding guarantee and a\n\t// rule they cannot see is not a chokepoint. Distinct from `hidden` because\n\t// they are different acts by different authorities: clearing one must not\n\t// clear the other.\n\tglobal          bool   // all-surface redaction; the deep link withholds too\n\tpurged          bool   // text destroyed; the stub remains\n\tglobalClearedAt     int64 // height a global act last cleared, for the re-set window\n\tglobalClearedAtTime int64 // unix seconds beside it; 0 on clears predating the stamps\n\t// No legalCode here. One was carried and read by nothing: boardActCode\n\t// already writes the category into the moderation log, and that copy is the\n\t// durable one — PurgeModLogRow destroys reasons, not codes. A second home on\n\t// the row for a fact that already has a durable one is a divergence risk with\n\t// no reader, and I3 argues against putting a legal code on the row's own page\n\t// in any case.\n}\n\n// boardMark is the one-character state a wire row carries, and the ONE place the\n// four states are distinguished. Three wire paths read it: a client that showed\n// \"withdrawn by its author\" over a statutory redaction would be stating something\n// false about a person, so the distinction is not cosmetic.\n//\n//\t.  readable      h  hidden by its author or a court mod\n//\tg  withheld      x  purged: the bytes are gone\nfunc boardMark(r *boardRow) string {\n\tswitch {\n\tcase r.purged:\n\t\treturn \"x\"\n\tcase r.global:\n\t\treturn \"g\"\n\tcase r.hiddenByAuthor || r.hiddenByMod:\n\t\treturn \"h\"\n\t}\n\treturn \".\"\n}\n\n// ensureBoard allocates the four indexes. WRITE PATHS ONLY — named in\n// check-read-purity.py's ALLOCATORS for that reason: a read that reached for it\n// would let the first caller change what the second one sees, and a query\n// carries no storage deposit, so it would be unpaid state growth.\nfunc ensureBoard(cs *claimState) {\n\tif cs.board == nil {\n\t\tcs.board = bptree.NewBPTree32()\n\t\tcs.boardTop = bptree.NewBPTree32()\n\t\tcs.boardWrote = bptree.NewBPTree32()\n\t}\n}\n\n// THREE OF THE SIX ARE ALLOCATED WHEN FIRST WRITTEN, not with the others.\n//\n// A claim whose board has one top-level comment and no replies and no upvotes —\n// which is most of them — used to carry three permanently empty trees forever.\n// The argument against lazy allocation elsewhere in this realm is that it gives\n// a reader a second branch which has to agree with the write path; here every\n// reader ALREADY nil-checks, because these three were always nil until the first\n// comment anyway. So the branch exists either way and the allocation is the only\n// thing being deferred.\n//\n// Kept as separate trees rather than merged into one tagged keyspace: see\n// boardKeyMine, whose note explains that be(N) becomes a prefix of row N's own\n// key the moment the two share a keyspace, and that the failure mode is a prefix\n// scan quietly returning a row it does not own.\nfunc ensureKids(cs *claimState) *bptree.BPTree {\n\tif cs.boardKids == nil {\n\t\tcs.boardKids = bptree.NewBPTree32()\n\t}\n\treturn cs.boardKids\n}\n\nfunc ensureScoreIdx(cs *claimState) *bptree.BPTree {\n\tif cs.boardScore == nil {\n\t\tcs.boardScore = bptree.NewBPTree32()\n\t}\n\treturn cs.boardScore\n}\n\nfunc ensureVoted(cs *claimState) *bptree.BPTree {\n\tif cs.boardVoted == nil {\n\t\tcs.boardVoted = bptree.NewBPTree32()\n\t}\n\treturn cs.boardVoted\n}\n\n// boardOpen reports whether a claim still takes comments.\n//\n// THE TERMINAL PREDICATE THE REALM ALREADY USES, not `rewardsOpened`: a claim can\n// end without ever opening the rewards — CloseDeadClaim sets `closed`, and provClose\n// sets `verdictAt` — so gating on the reward draw would leave two terminal states\n// accepting comments forever. votelock.gno gates on this same pair.\nfunc boardOpen(cs *claimState) bool { return cs.verdictAt == 0 \u0026\u0026 !cs.closed }\n\nfunc boardWroteBy(cs *claimState, who address) int64 {\n\tif cs.boardWrote == nil {\n\t\treturn 0\n\t}\n\tif v := cs.boardWrote.Get(string(who)); v != nil {\n\t\treturn v.(int64)\n\t}\n\treturn 0\n}\n\n// PostComment writes a comment, or a reply to one, and returns its row id.\n//\n// parentRow 0 is a top-level comment; anything else must name an existing\n// TOP-LEVEL row — depth one, enforced here rather than by convention.\nfunc PostComment(cur realm, courtSlug string, claimID uint64, parentRow uint64, text string) uint64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\t// A purged court accretes no new content, alongside the OpenClaim gate that\n\t// carve-out already covers. Tombstoning the render would not stop the rows\n\t// arriving.\n\tif courtIsPurged(c) {\n\t\tpanic(\"kourtv2: this court is purged; it takes no new content\")\n\t}\n\tcs := mustClaim(c, claimID)\n\tif !boardOpen(cs) {\n\t\tpanic(\"kourtv2: this claim is settled; its board is closed to new comments\")\n\t}\n\t// The two moderator freezes (boardmod.gno), read on every board WRITE and\n\t// nowhere else. A frozen address still stakes, votes, answers, disputes,\n\t// flags, opens claims, withdraws, draws emission and takes every election\n\t// action — the election is the remedy for a capturing set, so a freeze that\n\t// reached any of those could silence the vote that removes it.\n\tmustBoardWritable(c, cs, who)\n\ttext = strings.TrimSpace(text)\n\tif text == \"\" {\n\t\tpanic(\"kourtv2: a comment needs text\")\n\t}\n\tif runeLen(text) \u003e maxBoardTextLen {\n\t\tpanic(\"kourtv2: a comment is at most \" + strconv.Itoa(maxBoardTextLen) + \" characters\")\n\t}\n\n\t// EVERY REFUSAL BEFORE THE THROTTLE, so a rejected call costs no allowance.\n\t// The reverse order would let a mistyped parent id or an over-long body burn\n\t// a post the poster never made — and the allowance refills slowly enough that\n\t// this is a real cost rather than an inconvenience.\n\tif parentRow != 0 {\n\t\tp := mustBoardRow(cs, parentRow)\n\t\tif p.parent != 0 {\n\t\t\tpanic(\"kourtv2: replies are one level deep; reply to the comment, not to a reply\")\n\t\t}\n\t}\n\tif boardWroteBy(cs, who) \u003e= maxBoardRowsPerAuthor {\n\t\tpanic(\"kourtv2: you already have \" + strconv.Itoa(maxBoardRowsPerAuthor) +\n\t\t\t\" comments on this claim\")\n\t}\n\tif cs.board != nil \u0026\u0026 cs.board.Size() \u003e= maxBoardRowsPerClaim {\n\t\tpanic(\"kourtv2: this claim's board is full\")\n\t}\n\n\tmustSpendPost(c, who)\n\n\tensureBoard(cs)\n\tcs.boardNextID++\n\tr := \u0026boardRow{\n\t\tid: cs.boardNextID, parent: parentRow, author: who, text: text,\n\t\tat: heightNow(),\n\t}\n\tk := beClaimKey(r.id)\n\tcs.board.Set(k, r)\n\tif parentRow == 0 {\n\t\tcs.boardTop.Set(k, r)\n\t\tscoreIndexPut(cs, r)\n\t} else {\n\t\tensureKids(cs).Set(beClaimKey(parentRow)+k, r)\n\t}\n\tcs.boardWrote.Set(string(who), boardWroteBy(cs, who)+1)\n\treturn r.id\n}\n\nfunc mustBoardRow(cs *claimState, rowID uint64) *boardRow {\n\tif cs.board == nil {\n\t\tpanic(\"kourtv2: no such comment\")\n\t}\n\tv := cs.board.Get(beClaimKey(rowID))\n\tif v == nil {\n\t\tpanic(\"kourtv2: no such comment\")\n\t}\n\treturn v.(*boardRow)\n}\n\n// HideOwnComment lets an author withdraw their own row from the listings.\n//\n// A DISCOVERY BIT AND NOT A DELETE: the row, its id and its author stay, its own\n// route still serves it, and it still counts against its author's per-claim cap.\n// Otherwise self-hide would be a way to post nine comments by hiding one — and\n// deleting would be delete-the-evidence-after-the-quote.\nfunc HideOwnComment(cur realm, courtSlug string, claimID uint64, rowID uint64, hide bool) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tr := mustBoardRow(cs, rowID)\n\tif r.author != who {\n\t\tpanic(\"kourtv2: only a comment's author may hide their own comment\")\n\t}\n\tr.hiddenByAuthor = hide\n\t// Top holds only visible rows, so hiding leaves the index and unhiding\n\t// rejoins it at whatever score it had. Newest keeps the row either way —\n\t// that is the ordering nothing can be pushed off.\n\t//\n\t// scoreIndexPut refuses a row that is still hidden by the OTHER authority\n\t// (it gates on boardMark), so an author who un-withdraws a row a moderator\n\t// removed does not republish it. That refusal is the whole fix.\n\tif hide {\n\t\tscoreIndexDrop(cs, r)\n\t} else {\n\t\tscoreIndexPut(cs, r)\n\t}\n}\n\n// ------------------------------------------------------------------ reads --\n\n// BoardSize is the row count, top-level and replies together.\nfunc BoardSize(courtSlug string, claimID uint64) int {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tif cs.board == nil {\n\t\treturn 0\n\t}\n\treturn cs.board.Size()\n}\n\n// BoardCounts answers, for MANY claims in ONE query, the two numbers a reader\n// needs about a discussion without opening it:\n//\n//\t\"\u003cid\u003e:\u003crows\u003e:\u003cthreads\u003e;…\"\n//\n// rows is every row, top-level and replies together — the same number BoardSize\n// returns and the same one the claim page's own link advertises. threads is the\n// count of rows with no parent, so \"7 rows in 3 threads\" says both how much\n// there is to read and how many separate conversations it is.\n//\n// BOTH ARE O(1). cs.board and cs.boardTop are each a bptree, and Size() on a\n// bptree is a stored count rather than a walk — so this is two field reads per\n// claim and the cost of the reply is the string, not the counting. It adds NO\n// state: there is nothing to maintain at a write site and therefore nothing that\n// can drift, which is the trade BoardHiddenCount explains one level down.\n//\n// WHY IT IS PACKED, and it is the same reason DocketSeries is. The overlay's map\n// drew a cluster of dots beside each claim to show where the talking is, and\n// asked BoardSize once per node: twenty-one queries for twenty-one nodes, each\n// one reloading this realm. Batching at the RPC does not help, because gno\n// executes a JSON-RPC array serially, so the batching has to happen HERE where\n// the realm is loaded once for the whole page. Twenty-one queries become one.\n//\n// IT COSTS NO GAS. This is a read, so it is a query and not a transaction. The\n// alternative — a maintained counter — would have added gas to every post and\n// every purge in order to speed up something nobody pays for.\n//\n// ids is comma-separated, in any order; the reply keeps that order and SKIPS an\n// id this court does not have, so a caller matches on the id field rather than\n// on position. At most seriesPageMax ids are answered and the rest are dropped:\n// read the length of what came back.\n//\n// THREADS CAN EXCEED NOTHING AND NEVER EXCEEDS ROWS, which is worth saying\n// because the two trees are maintained separately: every top-level row is in\n// both, a reply is in cs.board alone. A claim with no board at all answers 0:0\n// rather than being skipped — it is a claim this court HAS, and a caller\n// drawing one cluster per node needs to hear about the quiet ones too.\nfunc BoardCounts(courtSlug string, ids string) string {\n\tc := mustCourt(courtSlug)\n\tout := strings.Builder{}\n\tsent := 0\n\tfor _, part := range strings.Split(ids, \",\") {\n\t\tif sent \u003e= seriesPageMax {\n\t\t\tbreak\n\t\t}\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tid, err := strconv.ParseUint(part, 10, 64)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tv := c.claims.Get(beClaimKey(id))\n\t\tif v == nil {\n\t\t\tcontinue // a claim this court does not have is skipped, never guessed\n\t\t}\n\t\tcs := v.(*claimState)\n\t\trows, threads := 0, 0\n\t\tif cs.board != nil {\n\t\t\trows = cs.board.Size()\n\t\t}\n\t\tif cs.boardTop != nil {\n\t\t\tthreads = cs.boardTop.Size()\n\t\t}\n\t\tif sent \u003e 0 {\n\t\t\tout.WriteString(\";\")\n\t\t}\n\t\tout.WriteString(strconv.FormatUint(id, 10))\n\t\tout.WriteString(\":\" + strconv.Itoa(rows))\n\t\tout.WriteString(\":\" + strconv.Itoa(threads))\n\t\tsent++\n\t}\n\treturn out.String()\n}\n\n// BoardHiddenCount is how many TOP-LEVEL rows this claim's list is not\n// showing — hidden, legally withheld or purged.\n//\n// TOP-LEVEL ONLY, and that is the whole reason it is not cs.board.Size() minus\n// something. A withheld REPLY is not missing from the list: it sits inside a\n// thread a reader may never open, and counting it would make the number\n// disagree with what the page is actually withholding from view. The list shows\n// parents, so the count is about parents.\n//\n// A SCAN AND NOT A COUNTER, which is the trade boardPartyRows already made and\n// for the same reason one level up. A counter would have to be maintained at\n// every site that changes a mark — HideOwnComment, HideBoardRow, UnhideBoardRow,\n// GlobalHideRow, PurgeBoardRow, PurgeBoardRange, GlobalClearBoardBits — and one\n// missed site drifts silently, forever, with nothing to notice. The walk is\n// bounded by the per-claim row cap and CANNOT disagree with the board it is\n// counting.\n//\n// It costs no gas either way: this is a read, and a read is a query rather than\n// a transaction. A counter would add gas to every hide and purge in order to\n// speed up something nobody pays for.\nfunc BoardHiddenCount(courtSlug string, claimID uint64) int {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tif cs.boardTop == nil {\n\t\treturn 0\n\t}\n\tn := 0\n\tcs.boardTop.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tif boardMark(v.(*boardRow)) != \".\" {\n\t\t\tn++\n\t\t}\n\t\treturn false\n\t})\n\treturn n\n}\n\n// BoardOpen reports whether the claim still takes comments.\nfunc BoardOpen(courtSlug string, claimID uint64) bool {\n\treturn boardOpen(mustClaim(mustCourt(courtSlug), claimID))\n}\n\n// CommentsWrittenBy is what an address has already spent of its per-claim cap.\nfunc CommentsWrittenBy(courtSlug string, claimID uint64, who address) int64 {\n\treturn boardWroteBy(mustClaim(mustCourt(courtSlug), claimID), who)\n}\n\n// BoardNewest is one page of top-level comments, newest first, packed one row\n// per line:\n//\n//\t\u003cid\u003e|\u003cauthor\u003e|\u003creplies\u003e|\u003cmark\u003e|\u003cat\u003e|\u003ctext\u003e\n//\n// `at` is the block height the comment landed. It is rendered on the row's own\n// page and was carried by NO exported read, so a client could offer \"newest\n// first\" and be unable to date a single row — half a citation, on a claim whose\n// market moves under it.\n//\n// NEWEST-FIRST IS THE ORDERING THAT CARRIES THE GUARANTEE. It is append-only, so\n// a prefix of it is stable across reads and an offset a client held from an\n// earlier page still points at the same row — the property stakeindex.gno makes\n// a contract of for its own index. The ranked ordering cannot promise that,\n// because one upvote between page one and page two moves a row across the\n// boundary; that is why there are two orderings and why this is the one nothing\n// can be pushed off.\n//\n// Hidden rows are returned with an empty text and an `h` marker rather than\n// dropped: dropping them would make the page cost O(offset + hidden), with the\n// hidden count chosen by whoever is hiding. The row stays, the text does not.\n//\n// THAT IS THE WIRE'S BARGAIN, NOT THE PAGE'S. A client is handed the marked rows\n// so its paging stays cheap and stable; what it DRAWS is its own choice, and\n// both renderers here drop them from the listing and collect them behind one\n// link. A moderator clearing twenty griefer comments should not leave twenty\n// stubs down the page — the stubs are more prominent than the comments were.\nfunc BoardNewest(courtSlug string, claimID uint64, offset, count int) string {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tif cs.boardTop == nil || count \u003c= 0 {\n\t\treturn \"\"\n\t}\n\tif count \u003e maxBoardRepliesShown*8 {\n\t\tcount = maxBoardRepliesShown * 8\n\t}\n\tvar b strings.Builder\n\tcs.boardTop.ReverseIterateByOffset(offset, count, func(_ string, v any) bool {\n\t\tr := v.(*boardRow)\n\t\tb.WriteString(strconv.FormatUint(r.id, 10) + \"|\" + r.author.String() + \"|\" +\n\t\t\tstrconv.Itoa(boardReplyCount(cs, r.id)) + \"|\")\n\t\tif m := boardMark(r); m != \".\" {\n\t\t\tb.WriteString(m + \"|\" + strconv.FormatInt(r.at, 10) + \"|\\n\")\n\t\t\treturn false\n\t\t}\n\t\tb.WriteString(\".|\" + strconv.FormatInt(r.at, 10) + \"|\" + boardTextFor(r) + \"\\n\")\n\t\treturn false\n\t})\n\treturn b.String()\n}\n\n// boardReplyCount counts one parent's direct replies. Bounded by the per-claim\n// row cap, and depth one means there is no recursion to bound.\nfunc boardReplyCount(cs *claimState, parent uint64) int {\n\tif cs.boardKids == nil {\n\t\treturn 0\n\t}\n\tpre := beClaimKey(parent)\n\tn := 0\n\tcs.boardKids.Iterate(pre, \"\", func(k string, _ any) bool {\n\t\tif !strings.HasPrefix(k, pre) || !boardKeyMine(pre, k) {\n\t\t\treturn true\n\t\t}\n\t\tn++\n\t\treturn false\n\t})\n\treturn n\n}\n\n// boardKeyMine is the length guard on every prefix scan here, and it is\n// REDUNDANT TODAY — recorded rather than papered over, because a guard nobody\n// can kill is a guard nobody should trust without knowing why.\n//\n// Why it cannot be killed: boardKids holds only 16-byte `be(parent)|be(rowID)`\n// keys, and the first eight bytes ARE the parent, so HasPrefix alone already\n// admits exactly that parent's children. There is no key in this tree that a\n// correct prefix matches and this test rejects.\n//\n// Why it stays: the redundancy is a property of the trees being SEPARATE. Merge\n// rows and children into one tree — the obvious economy, and the one\n// p/gnoland/boards invites with its flat AllReplies index — and `be(N)` becomes\n// a prefix of row N's own eight-byte key, so a scan for N's children would\n// return N itself. votelock.gno's pruneVoteLocks learned that lesson worse: it\n// read an unparseable prefix match as permission to DELETE. One definition, used\n// by every scan here, so the merge cannot half-happen.\nfunc boardKeyMine(pre, k string) bool { return len(k) == len(pre)+8 }\n\n// BoardReplies is one parent's direct replies, oldest first — a reply thread\n// reads in order, unlike the top-level list. Packed:\n//\n//\t\u003cid\u003e|\u003cauthor\u003e|\u003cmark\u003e|\u003cat\u003e|\u003ctext\u003e\n//\n// FOUR FIELDS BEFORE THE TEXT, not five: a reply carries no reply count of its\n// own, because depth stops at one. A client that reuses BoardNewest's arity here\n// reads this row's MARK as a reply count.\nfunc BoardReplies(courtSlug string, claimID uint64, parent uint64) string {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tif cs.boardKids == nil {\n\t\treturn \"\"\n\t}\n\tpre := beClaimKey(parent)\n\tvar b strings.Builder\n\tn := 0\n\tcs.boardKids.Iterate(pre, \"\", func(k string, v any) bool {\n\t\tif !strings.HasPrefix(k, pre) || !boardKeyMine(pre, k) {\n\t\t\treturn true\n\t\t}\n\t\tr := v.(*boardRow)\n\t\tb.WriteString(strconv.FormatUint(r.id, 10) + \"|\" + r.author.String() + \"|\")\n\t\tif m := boardMark(r); m != \".\" {\n\t\t\tb.WriteString(m + \"|\" + strconv.FormatInt(r.at, 10) + \"|\\n\")\n\t\t} else {\n\t\t\tb.WriteString(\".|\" + strconv.FormatInt(r.at, 10) + \"|\" + boardTextFor(r) + \"\\n\")\n\t\t}\n\t\tn++\n\t\treturn n \u003e= maxBoardRepliesShown\n\t})\n\treturn b.String()\n}\n\n// BoardPartyRows is the claim's author and its answerer, each with their newest\n// top-level comment — the same two rows the realm's own board page pins above\n// both orderings, served to clients that render their own. Packed:\n//\n//\t\u003cid\u003e|\u003cauthor\u003e|\u003crole\u003e|\u003cmark\u003e|\u003cat\u003e|\u003ctext\u003e\n//\n// ROLE STANDS WHERE BoardNewest CARRIES A REPLY COUNT and BoardTop carries a\n// score: field 3 is always what THIS ordering knows about the row. It is spelled\n// out — `author` or `answerer` — and it is packed rather than left to the client\n// to derive, because the two rows are order-dependent only while both exist. An\n// author who has not commented yields a single row that is the ANSWERER'S, and a\n// client that assumed \"the first row is the author\" would put the wrong name on\n// somebody's argument. That is the mistake a client already makes reading\n// BoardReplies at BoardNewest's arity, and the fix is the same one: say it on\n// the wire rather than leaving it to be inferred.\n//\n// IT SHARES boardPartyRows WITH THE RENDER, so this read and the realm's own\n// page cannot come to disagree about who the parties are.\n//\n// WHY IT EXISTS AT ALL, when a client could scan BoardNewest for the two\n// addresses it already holds: BoardNewest clamps count to 64 and a claim holds\n// up to maxBoardRowsPerClaim rows, so a party who commented FIRST and was buried\n// under later volume is off page one and silently omitted. That is precisely the\n// burial the reserved slot exists to prevent, so the client-side scan fails on\n// the one case the block is for.\n//\n// TOP-LEVEL ONLY, because boardTop holds parents: a party who has only ever\n// REPLIED gets no row here. That is the render's semantics too and deliberately\n// so — a reply pinned away from the thread it answers is an argument with its\n// question removed.\n//\n// THE PINNED ROW IS NOT FILTERED ON ITS MARK. A party's newest comment may be\n// hidden, withheld or purged, and it arrives with the mark set and EMPTY TEXT\n// like every other packing here. A client that renders the badge and then trusts\n// the text prints a name over nothing; the mark is how it knows to print the\n// tombstone instead.\n//\n// The answerer's gate is frozenAt != 0 — what HasAnswer reports, not the mere\n// presence of an address, because an unfrozen answer is not one the realm names\n// as an answer anywhere else either.\nfunc BoardPartyRows(courtSlug string, claimID uint64) string {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\t// REQUIRED, not defensive: boardPartyRows iterates boardTop unguarded, and a\n\t// claim nobody has commented on has no tree to iterate. The render reaches it\n\t// only past its own `boardTop == nil` early return; an exported read has no\n\t// such shelter.\n\tif cs.boardTop == nil {\n\t\treturn \"\"\n\t}\n\tvar b strings.Builder\n\tfor _, r := range boardPartyRows(cs) {\n\t\t// Derived from the ROW rather than from its position, which is what makes\n\t\t// the single-row case unambiguous. boardPartyRows skips an answerer who is\n\t\t// also the author, so no row can answer to both names.\n\t\trole := \"answerer\"\n\t\tif r.author == cs.author {\n\t\t\trole = \"author\"\n\t\t}\n\t\tb.WriteString(strconv.FormatUint(r.id, 10) + \"|\" + r.author.String() + \"|\" + role + \"|\")\n\t\tif m := boardMark(r); m != \".\" {\n\t\t\tb.WriteString(m + \"|\" + strconv.FormatInt(r.at, 10) + \"|\\n\")\n\t\t\tcontinue\n\t\t}\n\t\tb.WriteString(\".|\" + strconv.FormatInt(r.at, 10) + \"|\" + boardTextFor(r) + \"\\n\")\n\t}\n\treturn b.String()\n}\n\n// boardTextFor is THE single place a comment's text becomes output, and it is\n// named in check-render-text.py's census for that reason. A comment is\n// attacker-controlled multi-line text on a page people stake money on; every\n// consumer sanitises for its own context, and this is where the choice is made\n// once rather than at each caller.\nfunc boardTextFor(r *boardRow) string {\n\tif boardMark(r) != \".\" {\n\t\treturn \"\"\n\t}\n\t// ESCAPE THE ROW DELIMITER. A comment is multi-line BY DESIGN — that is why\n\t// the display gate below uses sanitize.Block rather than InlineText — and\n\t// every wire read here packs rows as \"…|…|text\\n\" with the text last. So a\n\t// comment whose text contained a newline did not merely wrap: it CLOSED its\n\t// own row and opened another, with an attacker-chosen id and author. Text\n\t// like \"x\\n999|g1attacker|0|.|forged\" forged a row in BoardNewest, BoardTop\n\t// and BoardReplies alike.\n\t//\n\t// Escaped here rather than refused at PostComment, because refusing newlines\n\t// would delete multi-line comments, and escaped at the SINGLE wire gate rather\n\t// than at each of the three packers, for the reason this function exists.\n\t// Pipes need no escape: text is the terminal field on every row, so a bounded\n\t// split recovers it whole.\n\treturn escapeWireText(r.text)\n}\n\n// escapeWireText makes a comment safe to be the last field of a newline-\n// terminated wire row. Backslash first, or the escape is itself forgeable:\n// \"\\\\\" + \"n\" would otherwise arrive indistinguishable from an escaped newline.\nfunc escapeWireText(s string) string {\n\tif !strings.ContainsAny(s, \"\\\\\\n\\r\") {\n\t\treturn s // the common case allocates nothing\n\t}\n\tvar b strings.Builder\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tswitch s[i] {\n\t\tcase '\\\\':\n\t\t\tb.WriteString(\"\\\\\\\\\")\n\t\tcase '\\n':\n\t\t\tb.WriteString(\"\\\\n\")\n\t\tcase '\\r':\n\t\t\tb.WriteString(\"\\\\r\")\n\t\tdefault:\n\t\t\tb.WriteByte(s[i])\n\t\t}\n\t}\n\treturn b.String()\n}\n\n// ------------------------------------------------------------------ render --\n\n// renderBoardPage is a claim's comments, newest first.\n//\n// EVERY PIECE OF USER TEXT GOES THROUGH boardTextVisible, which is the single\n// gate named in check-render-text.py's census. A comment is attacker-controlled\n// multi-line text on a page people stake money on, and gnoweb runs no HTML\n// sanitiser after the realm, so the choice of helper is made once here rather\n// than at each caller.\nfunc renderBoardPage(slug string, claimID uint64) string {\n\treturn renderBoardIn(slug, claimID, false)\n}\n\n// renderBoardTop is the same board, ranked. A SEPARATE PAGE rather than a second\n// section, and that is the fix for a real defect: the page used to print the\n// parties, then Top, then Newest, with no dedup — so a two-comment board rendered\n// five comment blocks, and because cold-start puts every un-upvoted row in the\n// score index at score 0, on any board with no upvotes Top and Newest were\n// byte-identical lists printed back to back.\n//\n// Two orderings survive intact, one click apart, which is what the ruling asks\n// for: Top is a VIEW, and being outranked on it is never being hidden, because\n// nothing can be pushed off Newest.\nfunc renderBoardTop(slug string, claimID uint64) string {\n\treturn renderBoardIn(slug, claimID, true)\n}\n\n// renderBoardHidden is the page the count links to: the rows this claim's\n// list is not showing, and what happened to each.\n//\n// IT SAYS WHICH AUTHORITY ACTED, which no client can. boardMark collapses\n// hiddenByAuthor and hiddenByMod into one `h` on the wire on purpose, so an\n// overlay reading that wire may not attribute the act — \"withdrawn by its\n// author\" over a moderator's hide is an assertion about a person that would be\n// false half the time. This page reads the two bits directly, so it can.\n//\n// THE TEXT IS HERE FOR A HIDE, AND ONLY FOR A HIDE. A hide is a DISCOVERY bit:\n// the bytes were never destroyed and the row's own deep link already serves them\n// to anyone who asks, so withholding them on this page would hide nothing and\n// only make the record harder to read than the chain already makes it. A PURGE\n// destroyed the bytes and a GLOBAL withholding is pending a legal\n// determination — reprinting either would be the realm undoing its own act in a\n// tidier format, so those get the reason alone.\nfunc renderBoardHidden(slug string, claimID uint64) string {\n\tc := mustCourt(slug)\n\tcs := mustClaim(c, claimID)\n\tbase := \"/r/kourt/kourtv2:\" + slug + \"/\" + strconv.FormatUint(claimID, 10)\n\tvar b strings.Builder\n\tb.WriteString(\"# Hidden comments on claim \" + strconv.FormatUint(claimID, 10) + \"\\n\\n\")\n\tb.WriteString(\"[← the comments](\" + base + \"/board)\\n\\n\")\n\tif cs.boardTop == nil {\n\t\tb.WriteString(\"_Nothing has been hidden here._\\n\")\n\t\treturn b.String()\n\t}\n\tn := 0\n\tcs.boardTop.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tr := v.(*boardRow)\n\t\tm := boardMark(r)\n\t\tif m == \".\" {\n\t\t\treturn false\n\t\t}\n\t\tn++\n\t\tb.WriteString(\"---\\n\\n**\" + sanitize.InlineText(r.author.String()) + \"**\")\n\t\tb.WriteString(\" · block \" + strconv.FormatInt(r.at, 10))\n\t\t// A purged row has no text to reach, so it gets no link to reach it.\n\t\tif m != \"x\" {\n\t\t\tb.WriteString(\" · [#\" + strconv.FormatUint(r.id, 10) + \"](\" + base +\n\t\t\t\t\"/board/\" + strconv.FormatUint(r.id, 10) + \")\")\n\t\t} else {\n\t\t\tb.WriteString(\" · #\" + strconv.FormatUint(r.id, 10))\n\t\t}\n\t\tb.WriteString(\"\\n\\n\")\n\t\t// WHY, AND THEN WHAT WAS SAID — but only where there is something to\n\t\t// say and saying it undoes nothing.\n\t\t//\n\t\t// A PURGE destroyed the bytes; there is no text to show and never will\n\t\t// be. A GLOBAL withholding is pending a legal determination, and this\n\t\t// page reprinting it would be the realm undoing its own act in a tidier\n\t\t// format. Those two get the reason alone.\n\t\t//\n\t\t// A HIDE is different in kind: it is a DISCOVERY bit, the text was never\n\t\t// destroyed, and the row's own deep link serves it to anyone who asks.\n\t\t// Withholding it here would hide nothing and only make the record harder\n\t\t// to read than the chain already makes it.\n\t\t//\n\t\t// THROUGH boardTextVisible, not sanitize.Block, even though the switch\n\t\t// has already established the row is neither purged nor global. That\n\t\t// function is the one place a comment's text becomes output and the one\n\t\t// place the withholding states are decided; reimplementing the decision\n\t\t// here would make this the second place, free to drift. check-render-text\n\t\t// enforces exactly that, and caught this line reading the raw field.\n\t\tswitch {\n\t\tcase r.purged:\n\t\t\tb.WriteString(\"\u003e _Removed on legal grounds. The text is gone from the record; \" +\n\t\t\t\t\"the row remains._\\n\\n\")\n\t\tcase r.global:\n\t\t\tb.WriteString(\"\u003e _Withheld pending a legal determination._\\n\\n\")\n\t\tcase r.hiddenByMod:\n\t\t\tb.WriteString(\"\u003e _Removed from the listing by this court's moderators._\\n\\n\")\n\t\t\tb.WriteString(boardTextVisible(r) + \"\\n\\n\")\n\t\tdefault:\n\t\t\tb.WriteString(\"\u003e _Withdrawn from the listing by its author._\\n\\n\")\n\t\t\tb.WriteString(boardTextVisible(r) + \"\\n\\n\")\n\t\t}\n\t\treturn false\n\t})\n\tif n == 0 {\n\t\tb.WriteString(\"_Nothing has been hidden here._\\n\")\n\t}\n\treturn b.String()\n}\n\n// boardPostLink is gnoweb's prefilled transaction form for PostComment.\n//\n// $help IS THE REALM'S OWN ACTION SURFACE and nothing here used it. gnoweb\n// renders a signable form from the query string, so a link is the whole\n// mechanism: no javascript, no wallet integration in the realm, and the\n// arguments a reader would otherwise have to type correctly are already right.\n//\n// parentRow 0 is a new comment; anything else is a reply to that row, which is\n// why every top-level row carries one of these too. Depth stops at one, so a\n// reply row gets no reply link — the realm would refuse it.\nfunc boardPostLink(slug string, claimID, parentRow uint64) string {\n\treturn \"/r/kourt/kourtv2$help\u0026func=PostComment\" +\n\t\t\"\u0026courtSlug=\" + slug +\n\t\t\"\u0026claimID=\" + strconv.FormatUint(claimID, 10) +\n\t\t\"\u0026parentRow=\" + strconv.FormatUint(parentRow, 10)\n}\n\nfunc renderBoardIn(slug string, claimID uint64, ranked bool) string {\n\tc := mustCourt(slug)\n\tcs := mustClaim(c, claimID)\n\tvar b strings.Builder\n\tb.WriteString(\"# Comments on claim \" + strconv.FormatUint(claimID, 10) + \"\\n\\n\")\n\tbase := \"/r/kourt/kourtv2:\" + slug + \"/\" + strconv.FormatUint(claimID, 10)\n\tb.WriteString(\"[← the claim](\" + base + \")\")\n\t// The ordering switch, with the current one named rather than linked.\n\tif ranked {\n\t\tb.WriteString(\" · **Top** · [Newest](\" + base + \"/board)\")\n\t} else {\n\t\tb.WriteString(\" · [Top](\" + base + \"/board/top) · **Newest**\")\n\t}\n\tb.WriteString(\"\\n\\n\")\n\n\t// HOW TO WRITE ONE, which no page in this realm said. The overlay builds\n\t// $help links for every action it offers; gnoweb readers had none, so the\n\t// board was readable and unwritable for anyone not running the overlay —\n\t// the same defect the board itself had before it rendered at all.\n\t//\n\t// The prose that used to sit here (\"Anyone with standing in this court — or\n\t// a one-off entry pass — may comment\") is gone: it explained the rules to\n\t// somebody who wanted to write, without telling them where to click, and the\n\t// rules are on the standing page that this line already links to.\n\tb.WriteString(\"[Write a comment](\" + boardPostLink(slug, claimID, 0) + \") · \" +\n\t\t\"[Where you stand](/r/kourt/kourtv2:\" + slug + \"/me) · \" +\n\t\thelpLink + \"\\n\\n\")\n\tif !boardOpen(cs) {\n\t\tb.WriteString(\"\u003e This claim is settled. Its board is closed to new comments;\\n\" +\n\t\t\t\"\u003e what is here stays.\\n\\n\")\n\t}\n\t// THE MODERATOR FREEZE, WHICH NOTHING RENDERED. ClaimBoardFrozenUntil existed\n\t// with no caller at all, so a frozen board was a silent wall that announced\n\t// itself only as a panic on somebody's write.\n\t// THE CLAIM'S OWN LEGAL STATE, said out loud. A claim redaction or purge does\n\t// NOT reach its board — the lanes are per-row on purpose, so that answering a\n\t// notice about one comment does not force the whole claim to be redacted\n\t// (§7.6). The converse is that the discussion of a redacted claim stays fully\n\t// readable, and neither a reader nor an operator answering a notice could tell\n\t// from this page. Now they can.\n\tif TextRedacted(c.id, claimID) {\n\t\tb.WriteString(\"\u003e **The claim itself is withheld or removed on legal grounds.**\\n\" +\n\t\t\t\"\u003e These comments are a separate record and are not covered by that act.\\n\\n\")\n\t}\n\tif at, until := claimBoardFrozenAt(c, claimID); until != 0 {\n\t\t// The date, with the block as its reference; a pre-stamp freeze has only\n\t\t// the block. Same shape as the address-freeze banner in posting.gno.\n\t\twhen := \"block \" + strconv.FormatInt(until, 10)\n\t\tif at != 0 {\n\t\t\twhen = strconv.FormatInt(at, 10) + \" (\" + when + \")\"\n\t\t}\n\t\tb.WriteString(\"\u003e **Comments are paused on this claim by this court's moderators**\\n\" +\n\t\t\t\"\u003e until \" + when + \". The claim itself is\\n\" +\n\t\t\t\"\u003e unaffected: answers, stakes and the verdict continue as normal.\\n\\n\")\n\t}\n\tif cs.boardTop == nil || cs.boardTop.Size() == 0 {\n\t\tb.WriteString(\"_No comments yet._\\n\")\n\t\treturn b.String()\n\t}\n\n\t// THE PARTIES FIRST, ON BOTH ORDERINGS. The claim's author and its answerer\n\t// hold a reserved place because the need behind the reserved slot is being\n\t// able to answer a question about your own claim and have the answer seen.\n\t//\n\t// It belongs on Newest too, and an earlier version of this render moved it to\n\t// the ranked page only, reasoning that recency already surfaces a party. That\n\t// is false the moment the party commented FIRST and volume arrived after —\n\t// which is the burial case the slot exists for, and the one the test asserts.\n\t//\n\t// What was actually wrong was the absence of `seen`: the page printed the\n\t// parties, then Top, then Newest, with no dedup, so a two-comment board\n\t// rendered five comment blocks and — because cold start puts every un-upvoted\n\t// row in the score index at score 0 — Top and Newest were byte-identical\n\t// lists printed back to back on any board with no upvotes.\n\tseen := map[uint64]bool{}\n\tif parties := boardPartyRows(cs); len(parties) \u003e 0 {\n\t\tb.WriteString(\"## The parties\\n\\n\")\n\t\tfor _, r := range parties {\n\t\t\tseen[r.id] = true\n\t\t\t// A party's newest comment may itself be hidden, and a reserved slot\n\t\t\t// filled with a stub is the same clutter one line higher up.\n\t\t\tif boardMark(r) != \".\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb.WriteString(renderBoardOne(slug, claimID, cs, r, true))\n\t\t}\n\t\tif ranked {\n\t\t\tb.WriteString(\"\\n## Ranked\\n\\n\")\n\t\t} else {\n\t\t\tb.WriteString(\"\\n## Newest\\n\\n\")\n\t\t}\n\t}\n\n\tshown := 0\n\tif ranked {\n\t\tif cs.boardScore != nil {\n\t\t\tcs.boardScore.IterateByOffset(0, maxBoardRepliesShown*3, func(_ string, v any) bool {\n\t\t\t\tr := v.(*boardRow)\n\t\t\t\tif seen[r.id] {\n\t\t\t\t\treturn false // reserved above; a slot is not a second copy\n\t\t\t\t}\n\t\t\t\tb.WriteString(renderBoardOne(slug, claimID, cs, r, true))\n\t\t\t\tshown++\n\t\t\t\treturn false\n\t\t\t})\n\t\t}\n\t\tif shown == 0 {\n\t\t\tb.WriteString(\"_Nothing else upvoted yet — [Newest](\" + base +\n\t\t\t\t\"/board) is the fuller view._\\n\")\n\t\t}\n\t\t// Both orderings, or a reader who switched to Top would be told the board\n\t\t// holds fewer rows than the other view accounted for.\n\t\twriteHiddenLink(\u0026b, slug, claimID, base)\n\t\treturn b.String()\n\t}\n\n\tcs.boardTop.ReverseIterateByOffset(0, maxBoardRepliesShown*3, func(_ string, v any) bool {\n\t\tr := v.(*boardRow)\n\t\tif seen[r.id] || boardMark(r) != \".\" {\n\t\t\treturn false // hidden rows are counted and linked above, not stubbed here\n\t\t}\n\t\tb.WriteString(renderBoardOne(slug, claimID, cs, r, true))\n\t\tshown++\n\t\treturn false\n\t})\n\tif rest := cs.boardTop.Size() - shown - len(seen); rest \u003e 0 {\n\t\tb.WriteString(\"\\n_\" + strconv.Itoa(rest) + \" older comments._\\n\")\n\t}\n\twriteHiddenLink(\u0026b, slug, claimID, base)\n\treturn b.String()\n}\n\n// writeHiddenLink is the footnote for rows this page is not showing.\n//\n// AT THE FOOT, not the head. Hidden rows leave the listing entirely — collected\n// behind one link rather than left as a stub apiece, because a moderator\n// clearing twenty griefer comments used to leave twenty notices, each louder\n// than the comment it replaced. But the link to them is not what a reader came\n// for either, and above the first comment it was the third thing on the page.\n// It is a footnote about what is absent; it belongs after what is present.\nfunc writeHiddenLink(b *strings.Builder, slug string, claimID uint64, base string) {\n\tif k := BoardHiddenCount(slug, claimID); k \u003e 0 {\n\t\tb.WriteString(\"\\n[\" + strconv.Itoa(k) + \" hidden comment\" + plural2(k) +\n\t\t\t\"](\" + base + \"/board/hidden)\\n\")\n\t}\n}\n\n// boardPartyRows is the newest top-level comment by the claim's author and by\n// its answerer, in that order, skipping either who has not commented.\n//\n// A SCAN AND NOT AN INDEX, because it is bounded by the per-claim row cap and\n// runs once per page: an index keyed by author would be a fifth tree maintained\n// on every write to save a walk of at most maxBoardRowsPerClaim rows on a read.\n// stakeindex.gno's note is about the opposite trade and measured it; this one\n// falls the other way.\nfunc boardPartyRows(cs *claimState) []*boardRow {\n\twant := []address{cs.author}\n\tif cs.frozenAt != 0 \u0026\u0026 cs.answerer != \"\" \u0026\u0026 cs.answerer != cs.author {\n\t\twant = append(want, cs.answerer)\n\t}\n\tvar out []*boardRow\n\tfor _, who := range want {\n\t\tvar newest *boardRow\n\t\tcs.boardTop.ReverseIterate(\"\", \"\", func(_ string, v any) bool {\n\t\t\tr := v.(*boardRow)\n\t\t\tif r.author == who {\n\t\t\t\tnewest = r\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn false\n\t\t})\n\t\tif newest != nil {\n\t\t\tout = append(out, newest)\n\t\t}\n\t}\n\treturn out\n}\n\n// renderBoardRow is the per-row deep link, and it is MANDATORY rather than a\n// convenience: a courtMod hide is a discovery bit, and I3 forbids a discovery\n// bit removing text from a deep link. A hidden row is listed as hidden and still\n// reads here.\nfunc renderBoardRow(slug string, claimID uint64, rowArg string) string {\n\tid, err := strconv.ParseUint(rowArg, 10, 64)\n\tif err != nil {\n\t\treturn \"## Not found\\nA comment id must be a number.\"\n\t}\n\tc := mustCourt(slug)\n\tcs := mustClaim(c, claimID)\n\tif cs.board == nil || cs.board.Get(beClaimKey(id)) == nil {\n\t\treturn \"## Not found\\nNo comment by that id on this claim.\"\n\t}\n\tr := mustBoardRow(cs, id)\n\tvar b strings.Builder\n\t// WHAT THIS COMMENT IS ABOUT. A single row is the most-shared unit here —\n\t// somebody quotes an argument and sends the link — and this page opened\n\t// \"# Comment 1\" over \"← the board\" and said nothing else. A reader arriving\n\t// cold got an opinion with no proposition attached: not which claim, not\n\t// which court, and no way to find out but to click a link labelled with a\n\t// word that tells them nothing either.\n\t//\n\t// The claim's TITLE is the heading, because that is the thing being argued\n\t// about; the comment number is demoted to a subtitle, where an ordinal\n\t// belongs. claimTitleFor gates the title the same way the claim's own page\n\t// does, so a redacted or purged claim does not leak its text here.\n\tb.WriteString(\"# \" + claimTitleFor(c, cs) + \"\\n\\n\")\n\tbase := \"/r/kourt/kourtv2:\" + slug + \"/\" + strconv.FormatUint(claimID, 10)\n\tb.WriteString(\"Comment \" + strconv.FormatUint(id, 10) + \" on this claim in \" +\n\t\t\"[\" + courtNameFor(c) + \"](/r/kourt/kourtv2:\" + slug + \")\\n\\n\")\n\tb.WriteString(\"[← the claim](\" + base + \") · [all \" +\n\t\tstrconv.Itoa(cs.board.Size()) + \" comments](\" + base + \"/board)\\n\\n\")\n\tb.WriteString(renderBoardOne(slug, claimID, cs, r, false))\n\treturn b.String()\n}\n\n// renderBoardOne is one row. `listing` distinguishes the two surfaces: a hidden\n// row is withheld from a LISTING and served on its own route, which is the whole\n// content of the hide bit.\nfunc renderBoardOne(slug string, claimID uint64, cs *claimState, r *boardRow, listing bool) string {\n\tvar b strings.Builder\n\tb.WriteString(\"---\\n\\n**\" + sanitize.InlineText(r.author.String()) + \"**\")\n\tfor _, badge := range boardBadges(cs, r.author) {\n\t\tb.WriteString(\" · \" + badge)\n\t}\n\tb.WriteString(\" · [#\" + strconv.FormatUint(r.id, 10) + \"](/r/kourt/kourtv2:\" + slug + \"/\" +\n\t\tstrconv.FormatUint(claimID, 10) + \"/board/\" + strconv.FormatUint(r.id, 10) + \")\" +\n\t\t\" · block \" + strconv.FormatInt(r.at, 10) + \"\\n\\n\")\n\tif (r.hiddenByAuthor || r.hiddenByMod) \u0026\u0026 !r.global \u0026\u0026 !r.purged \u0026\u0026 listing {\n\t\t// WHICH AUTHORITY, because the two are different facts about a person.\n\t\t// A moderator act reported as \"withdrawn by its author\" is an assertion\n\t\t// this realm has no business making, and MODERATION §6's mod-copy rule\n\t\t// is exactly about not making it.\n\t\tif r.hiddenByMod {\n\t\t\tb.WriteString(\"\u003e _Removed from this listing by this court's moderators. \" +\n\t\t\t\t\"It still reads at its own link._\\n\\n\")\n\t\t} else {\n\t\t\tb.WriteString(\"\u003e _Withdrawn from this listing. It still reads at its own link._\\n\\n\")\n\t\t}\n\t\treturn b.String()\n\t}\n\tb.WriteString(boardTextVisible(r) + \"\\n\\n\")\n\tif r.parent == 0 {\n\t\twriteBoardReplies(\u0026b, slug, claimID, cs, r.id)\n\t\t// TOP-LEVEL ONLY. Depth stops at one, so offering a reply link on a reply\n\t\t// would be a form the realm refuses — a button that cannot work is worse\n\t\t// than no button, because it teaches a reader the wrong shape.\n\t\t//\n\t\t// Only while the board is open, for the same reason: a settled claim's\n\t\t// board takes no comments, and PostComment would refuse this too.\n\t\tif boardOpen(cs) {\n\t\t\tb.WriteString(\"[Reply](\" + boardPostLink(slug, claimID, r.id) + \")\\n\\n\")\n\t\t}\n\t}\n\treturn b.String()\n}\n\n// writeBoardReplies renders a top-level comment's direct replies, indented.\n//\n// THE SECOND LEVEL OF THE TREE WAS WRITE-ONLY. The page printed \"_1 reply._\" and\n// stopped — the count was not even a link, and BoardReplies, the one function\n// that reads them, had no caller on any render path. Replies could be posted and\n// never read on the site, which is not a discussion board.\n//\n// Bounded by maxBoardRepliesShown, and depth one means there is no recursion to\n// bound. Each reply goes through boardTextVisible like every other piece of user\n// text, and carries its own deep link, because a reply is quotable evidence too.\nfunc writeBoardReplies(b *strings.Builder, slug string, claimID uint64, cs *claimState, parent uint64) {\n\t// NO nil check on boardKids here, and that is deliberate rather than an\n\t// oversight: boardReplyCount nil-guards already, so a claim with no replies\n\t// index answers 0 and the next line returns before anything is dereferenced.\n\t// A third guard would be one the corpus cannot catch, because it cannot\n\t// change an answer — the count IS the guard.\n\tpre := beClaimKey(parent)\n\tn, total := 0, boardReplyCount(cs, parent)\n\tif total == 0 {\n\t\treturn\n\t}\n\tcs.boardKids.Iterate(pre, \"\", func(k string, v any) bool {\n\t\tif !strings.HasPrefix(k, pre) || !boardKeyMine(pre, k) {\n\t\t\treturn true\n\t\t}\n\t\tr := v.(*boardRow)\n\t\t// A hidden reply leaves the thread for the same reason a hidden\n\t\t// top-level row leaves the list: the stub is louder than what it\n\t\t// replaced. BoardHiddenCount does not count these — a reply is not\n\t\t// missing from a list it was never in — so the link above does not\n\t\t// promise them either.\n\t\tif boardMark(r) != \".\" {\n\t\t\treturn false\n\t\t}\n\t\tb.WriteString(\"\u003e **\" + sanitize.InlineText(r.author.String()) + \"**\")\n\t\tfor _, badge := range boardBadges(cs, r.author) {\n\t\t\tb.WriteString(\" · \" + badge)\n\t\t}\n\t\tb.WriteString(\" · [#\" + strconv.FormatUint(r.id, 10) + \"](/r/kourt/kourtv2:\" + slug +\n\t\t\t\"/\" + strconv.FormatUint(claimID, 10) + \"/board/\" + strconv.FormatUint(r.id, 10) + \")\\n\u003e\\n\")\n\t\t{\n\t\t\t// The reply's own text, quoted one level. Block-sanitised like every\n\t\t\t// other comment; the \"\u003e \" prefix is ours, not the author's.\n\t\t\tfor _, line := range strings.Split(boardTextVisible(r), \"\\n\") {\n\t\t\t\tb.WriteString(\"\u003e \" + line + \"\\n\")\n\t\t\t}\n\t\t\tb.WriteString(\"\\n\")\n\t\t}\n\t\tn++\n\t\treturn n \u003e= maxBoardRepliesShown\n\t})\n\tif total \u003e n {\n\t\tb.WriteString(\"_\" + strconv.Itoa(total-n) + \" more repl\" + plural(total-n) +\n\t\t\t\" at [#\" + strconv.FormatUint(parent, 10) + \"](/r/kourt/kourtv2:\" + slug + \"/\" +\n\t\t\tstrconv.FormatUint(claimID, 10) + \"/board/\" + strconv.FormatUint(parent, 10) + \")._\\n\\n\")\n\t}\n}\n\nfunc plural(n int) string {\n\tif n == 1 {\n\t\treturn \"y\"\n\t}\n\treturn \"ies\"\n}\n\n// boardBadges says what a commenter has at stake on THIS claim — author,\n// answerer, or a position and its side.\n//\n// COMPOSITION MADE LEGIBLE RATHER THAN CURATED. The ordering does not rank by\n// this; the reader is told who has money on the question and decides for\n// themselves. All four facts are already stored, so the badge costs a lookup.\nfunc boardBadges(cs *claimState, who address) []string {\n\tvar out []string\n\tif who == cs.author {\n\t\tout = append(out, \"author\")\n\t}\n\tif cs.frozenAt != 0 \u0026\u0026 who == cs.answerer {\n\t\tout = append(out, \"answerer\")\n\t}\n\tfor _, side := range []int{sideYES, sideNO} {\n\t\tif v := cs.stakers.Get(posKey(who, side)); v != nil {\n\t\t\tif p := v.(*stakePos); p.stake \u003e 0 {\n\t\t\t\tname := \"YES\"\n\t\t\t\tif side == sideNO {\n\t\t\t\t\tname = \"NO\"\n\t\t\t\t}\n\t\t\t\tout = append(out, name+\" \"+strconv.FormatInt(p.stake, 10))\n\t\t\t}\n\t\t}\n\t}\n\treturn out\n}\n\n// boardTextVisible is THE single place a comment becomes display text.\n//\n// sanitize.Block, the STRICT variant, for the reason claimBodyVisible's note\n// gives about a claim body: a comment is multi-line by design so InlineText is\n// wrong, and Block escapes the doc-spoof markers that BlockRich deliberately\n// preserves. A comment is lower-trust than a claim body, not higher.\nfunc boardTextVisible(r *boardRow) string {\n\t// The legal states first, and they say WHICH one: \"removed\" over a purge and\n\t// \"withheld\" over a redaction are different facts, and the second is\n\t// reversible by a counter-notice while the first is not.\n\tif r.purged {\n\t\treturn \"\u003e _Removed. The text is gone from the record; the row remains._\"\n\t}\n\tif r.global {\n\t\treturn \"\u003e _Withheld pending a legal determination._\"\n\t}\n\tif r.text == \"\" {\n\t\treturn \"_(empty)_\"\n\t}\n\treturn sanitize.Block(r.text)\n}\n\n// ---- Top: the ranked ordering ---------------------------------------------\n//\n// TWO ORDERINGS, AND NEITHER IS THE ONLY PAGE ONE. That is the whole design.\n// MODERATION.md §3.2 refuses an auto-ranking on any address- or vote-keyed\n// signal for the directory, calling it a capture surface, and gives two\n// paginated orderings from two monotone indexes instead. This is the same\n// arrangement one level down: Top ranks by who upvoted, Newest cannot be pushed\n// off, and capturing Top buys a VIEW rather than the surface.\n//\n// WHY STANDING IS A DEFENSIBLE KEY WHERE STAKE WAS NOT. Stake is rentable — this\n// is a no-loss realm, principal returns 1x either way, so holding the top of a\n// claim costs only carry and the rank collapses for everyone at settlement.\n// Standing cannot be rented, cannot be transferred, and does not survive\n// splitting: twenty addresses sharing one holder's standing cast twenty votes\n// worth a twentieth each.\n//\n// COLD START NEEDS NO CONSTANT. Every un-voted row scores zero and ties break on\n// the row id, so descending iteration over a court with no standing is EXACTLY\n// newest-first. Top degenerates into Newest rather than into noise, which is why\n// there is no rankColdStartN here despite the plan naming one.\n\n// boardScoreKey sorts high score first, and newest first within a score. Both\n// halves are inverted because bptree iterates ascending, and beInv is the\n// realm's one descending-key idiom (modrender.gno) rather than a second spelling\n// of it.\nfunc boardScoreKey(score int64, rowID uint64) string {\n\treturn beInv(score) + beInv(int64(rowID))\n}\n\n// scoreIndexPut and scoreIndexDrop are the ONLY writers of the score index.\n//\n// THE STORED KEY IS REMOVED, NEVER A RECOMPUTED ONE. reindexBurn does the same\n// and for the same reason: a key rebuilt from a score that has since changed\n// misses, and the tree keeps a second entry for one row — which then renders\n// twice, at two different ranks.\n//\n// THE INDEX HOLDS ONLY VISIBLE ROWS. Filtering at read time is not an option\n// here: bptree's IterateByOffset counts every row it VISITS, not every row the\n// callback keeps (tree.gno increments `visited` whatever the callback returns),\n// so a page that skipped hidden rows would short-page by an amount whoever is\n// hiding gets to choose. Keeping them out of the index also stops a tombstone\n// holding a rank it can no longer justify.\nfunc scoreIndexPut(cs *claimState, r *boardRow) {\n\t// Any withholding state keeps the row out: Top must not carry a rank for text\n\t// nobody may read, and a clear must not re-index a row the OTHER authority\n\t// still has hidden.\n\tif r.parent != 0 || boardMark(r) != \".\" {\n\t\treturn\n\t}\n\tr.scoreKey = boardScoreKey(r.score, r.id)\n\tensureScoreIdx(cs).Set(r.scoreKey, r)\n}\n\nfunc scoreIndexDrop(cs *claimState, r *boardRow) {\n\tif cs.boardScore == nil || r.scoreKey == \"\" {\n\t\treturn\n\t}\n\tcs.boardScore.Remove(r.scoreKey)\n\tr.scoreKey = \"\"\n}\n\n// UpvoteComment adds the caller's standing to a top-level comment's score.\n//\n// UPVOTE ONLY. A downvote is burial handed to anyone with standing, and burial\n// already belongs to moderators behind a threshold.\n//\n// The weight is SNAPSHOTTED. A later slash does not re-sort every board in the\n// court — which is a real cost, honestly: a cast vote is an effect a sanction\n// cannot reach, so §4's \"no banked balance a sanction fails to reach\" is true of\n// throughput and false of rank. Bounded, not fixed, by Top not being the only\n// view.\nfunc UpvoteComment(cur realm, courtSlug string, claimID uint64, rowID uint64) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tif courtIsPurged(c) {\n\t\tpanic(\"kourtv2: this court is purged; it takes no new content\")\n\t}\n\tcs := mustClaim(c, claimID)\n\tif !boardOpen(cs) {\n\t\tpanic(\"kourtv2: this claim is settled; its board is closed\")\n\t}\n\t// The two moderator freezes (boardmod.gno), read on every board WRITE and\n\t// nowhere else. A frozen address still stakes, votes, answers, disputes,\n\t// flags, opens claims, withdraws, draws emission and takes every election\n\t// action — the election is the remedy for a capturing set, so a freeze that\n\t// reached any of those could silence the vote that removes it.\n\tmustBoardWritable(c, cs, who)\n\tr := mustBoardRow(cs, rowID)\n\tif r.parent != 0 {\n\t\tpanic(\"kourtv2: only top-level comments are ranked; a reply is read under its parent\")\n\t}\n\tif r.author == who {\n\t\tpanic(\"kourtv2: you cannot upvote your own comment\")\n\t}\n\t// A PURGED ROW TAKES NO VOTES. Hidden and redacted rows do, deliberately —\n\t// both are reversible, and scoreIndexPut re-ranks them correctly when they\n\t// come back. A purge is the one state that never comes back: the bytes are\n\t// gone and the row is out of the ranked index for ever, so the vote's effect\n\t// is unreachable by construction and the allowance it costs buys nothing.\n\t// The realm refuses charges for nothing everywhere else; this is that rule.\n\tif r.purged {\n\t\tpanic(\"kourtv2: that comment was destroyed on legal grounds; it takes no votes\")\n\t}\n\tvk := beClaimKey(rowID) + string(who)\n\tif cs.boardVoted != nil \u0026\u0026 cs.boardVoted.Has(vk) {\n\t\tpanic(\"kourtv2: you have already upvoted that comment\")\n\t}\n\tvar w int64\n\tif sr := lookupStanding(c, who); sr != nil {\n\t\tw = sr.score\n\t}\n\tif w \u003c= 0 {\n\t\tpanic(\"kourtv2: an upvote is weighted by standing, and you have none here yet\")\n\t}\n\t// A vote is a state write, so it is its own flood surface and draws on the\n\t// same allowance a comment does.\n\tmustSpendPost(c, who)\n\n\tensureBoard(cs)\n\tensureVoted(cs).Set(vk, true)\n\tscoreIndexDrop(cs, r)\n\tr.score = satAdd(r.score, w)\n\tscoreIndexPut(cs, r)\n\treturn r.score\n}\n\n// HasUpvoted reports whether an address has already spent its vote on a row.\nfunc HasUpvoted(courtSlug string, claimID uint64, rowID uint64, who address) bool {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tif cs.boardVoted == nil {\n\t\treturn false\n\t}\n\treturn cs.boardVoted.Has(beClaimKey(rowID) + string(who))\n}\n\n// CommentScore is one row's accumulated weight.\nfunc CommentScore(courtSlug string, claimID uint64, rowID uint64) int64 {\n\treturn mustBoardRow(mustClaim(mustCourt(courtSlug), claimID), rowID).score\n}\n\n// BoardTop is one page of top-level comments, best first:\n//\n//\t\u003cid\u003e|\u003cauthor\u003e|\u003cscore\u003e|\u003cmark\u003e|\u003cat\u003e|\u003ctext\u003e\n//\n// The SCORE stands where BoardNewest carries a reply count, and the mark is\n// always \".\" — so the two orderings are the same arity and NOT the same fields.\n//\n// No hidden rows appear, because none are in the index — see scoreIndexPut.\nfunc BoardTop(courtSlug string, claimID uint64, offset, count int) string {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tif cs.boardScore == nil || count \u003c= 0 {\n\t\treturn \"\"\n\t}\n\tif count \u003e maxBoardRepliesShown*8 {\n\t\tcount = maxBoardRepliesShown * 8\n\t}\n\tvar b strings.Builder\n\tcs.boardScore.IterateByOffset(offset, count, func(_ string, v any) bool {\n\t\tr := v.(*boardRow)\n\t\tb.WriteString(strconv.FormatUint(r.id, 10) + \"|\" + r.author.String() + \"|\" +\n\t\t\tstrconv.FormatInt(r.score, 10) + \"|.|\" + strconv.FormatInt(r.at, 10) +\n\t\t\t\"|\" + boardTextFor(r) + \"\\n\")\n\t\treturn false\n\t})\n\treturn b.String()\n}\n\n// writeBoardLink is the claim page's one line about its board: how many\n// comments, and where they are.\n//\n// It says the COUNT, because \"Comments\" alone cannot distinguish an empty board\n// from a busy one, and the difference is the whole reason to click. It appears\n// even at zero, because a board nobody has written on is exactly the one worth\n// inviting somebody to.\nfunc writeBoardLink(b *strings.Builder, c *Court, cs *claimState, claimID uint64) {\n\t// EVERY ROW, not the top-level ones. This counted cs.boardTop, which is the\n\t// pagination index and holds only rows with no parent — so a board with\n\t// three threads and two replies advertised \"3 comments\" and then showed\n\t// five things to read. The number under a link is a promise about what is\n\t// behind it, and a reply is a comment: it is written by a person, it is\n\t// counted against their daily allowance, it can be hidden, upvoted and\n\t// purged like any other row, and a reader scanning the page counts it.\n\t//\n\t// cs.board is the tree of all rows, so this also counts the ones that\n\t// render as tombstones — hidden, legally withheld, purged. That is right for\n\t// the same reason: they occupy a line on the page the reader is about to\n\t// open, and a count that skipped them would undershoot again.\n\t//\n\t// IT ALSO AGREES WITH BoardSize NOW, which has always returned\n\t// cs.board.Size(). A client reading the realm and a reader looking at the\n\t// realm's own page were being told different numbers for the same board.\n\tn := 0\n\tif cs.board != nil {\n\t\tn = cs.board.Size()\n\t}\n\tlink := \"/r/kourt/kourtv2:\" + c.id + \"/\" + strconv.FormatUint(claimID, 10) + \"/board\"\n\tswitch {\n\tcase n == 0 \u0026\u0026 !boardOpen(cs):\n\t\treturn // a settled claim with no comments has nothing to offer\n\tcase n == 0:\n\t\tb.WriteString(\"## Comments\\n\\n[Write the first one](\" +\n\t\t\tboardPostLink(c.id, claimID, 0) + \")\\n\\n\")\n\t\treturn\n\t}\n\t// THE COMMENTS THEMSELVES, not a link to them. A claim page that said\n\t// \"5 comments →\" made a reader leave the page they came to read in order to\n\t// find out whether leaving was worth it. The discussion belongs under the\n\t// thing being discussed, which is where every other discussion on the web\n\t// puts it and where the overlay already put it.\n\t//\n\t// BOUNDED AT claimBoardPreview TOP-LEVEL ROWS, so a busy claim does not turn\n\t// its own page into a forum, and the tail says how many are behind the link\n\t// with the real number on it. The parties are NOT pinned here: this is a\n\t// preview of the newest, and the reserved slot is a property of the board\n\t// page, where the whole list competes for attention.\n\tb.WriteString(\"## Comments\\n\\n\")\n\tb.WriteString(\"[Write a comment](\" + boardPostLink(c.id, claimID, 0) + \")\\n\\n\")\n\tshown, drawn := 0, 0\n\tif cs.boardTop != nil {\n\t\t// COUNTED AFTER THE FILTER, not before. ReverseIterateByOffset(0, 3) takes\n\t\t// the newest three ROWS and this loop then discards the hidden ones, so a\n\t\t// claim whose two newest rows were hidden previewed ONE comment while\n\t\t// linking to \"all 7\" — the slots were spent on rows that never render.\n\t\t// Walking until three have been DRAWN is what the cap was always meant\n\t\t// to say. It stops as soon as it has them, so the common case reads the\n\t\t// same three rows it always did.\n\t\tcs.boardTop.ReverseIterate(\"\", \"\", func(_ string, v any) bool {\n\t\t\tr := v.(*boardRow)\n\t\t\tif boardMark(r) != \".\" {\n\t\t\t\treturn false // hidden rows are a footnote, not a preview\n\t\t\t}\n\t\t\tb.WriteString(renderBoardOne(c.id, claimID, cs, r, true))\n\t\t\t// THE REPLIES COUNT TOO, because renderBoardOne draws them nested\n\t\t\t// under this row. Counting only parents while `n` counts every row\n\t\t\t// made a claim that was showing BOTH its comments announce \"all 2\" —\n\t\t\t// the same off-by-a-category the overlay's own tail had, where a\n\t\t\t// fully-displayed board claimed to be hiding something.\n\t\t\tshown += 1 + boardReplyCount(cs, r.id)\n\t\t\tdrawn++\n\t\t\treturn drawn \u003e= claimBoardPreview\n\t\t})\n\t}\n\t// THE COUNT IS ALWAYS STATED, whether or not the preview is short of it. It\n\t// only read \"all 5 comments\" while something was hidden behind the link, so\n\t// a claim showing everything it had stopped saying how much that was — and\n\t// the number is a fact about the claim, not an apology for truncation.\n\tall := \"\"\n\tif shown \u003c n {\n\t\tall = \"all \"\n\t}\n\tb.WriteString(\"\\n[\" + all + strconv.Itoa(n) + \" comment\" + plural2(n) + \"](\" + link +\n\t\t\") · [Top](\" + link + \"/top)\\n\\n\")\n\twriteHiddenLink(b, c.id, claimID, \"/r/kourt/kourtv2:\"+c.id+\"/\"+strconv.FormatUint(claimID, 10))\n}\n\n// How many top-level comments a CLAIM page previews. Three, not the board's\n// twenty-four: this is a page about a claim that happens to have a discussion,\n// and the whole discussion has its own page one link away.\nconst claimBoardPreview = 3\n\nfunc plural2(n int) string {\n\tif n == 1 {\n\t\treturn \"\"\n\t}\n\treturn \"s\"\n}\n"},{"name":"boardlegal.gno","body":"// The board's legal lane: the two verbs a takedown needs, and the one that\n// destroys text (docs/CLAIM_BOARDS.md §6.8).\n//\n// WHY A ROW-LEVEL PAIR AT ALL. GlobalHide and PurgeClaim are keyed to a CLAIM,\n// so the DMCA answer to one infringing comment would be to redact the claim's\n// title and every other comment on it — over-redaction as the only fast lane,\n// which is how a notice about one row silences a whole discussion. And the\n// courtMod row hide is discovery-only by design: the deep link still serves, so\n// it does not \"disable access\" and is not the verb a notice asks for.\n//\n// TWO CODE SETS, AND THE SPLIT IS DELIBERATE. Editorial acts — hide, slash,\n// freeze — take a CLOSED code from mustBoardCode: they target a person, MODERATION\n// §6's mod-copy rule forbids assertions of fact about one, and there is no\n// \"claim-about-the-claim\" phrasing available for \"this address was slashed\". The\n// LEGAL acts here keep the length-bounded, extensible code the rest of the realm\n// uses, because I11 requires the purge code set to stay shape-extensible before\n// deploy and a closed switch on a write-once realm cannot record the first\n// statutory category nobody anticipated.\n\npackage kourtv2\n\nimport \"strconv\"\n\n// boardActCode is the code a board legal act writes into the moderation log.\n//\n// THE ROW ID GOES IN THE CODE, NOT THE REASON. PurgeModLogRow tombstones a log\n// row's REASON, so an id kept there would be destroyed by the very verb whose\n// job is removing text — and the log would then say a comment was redacted\n// without saying which. The id is metadata about the act, not the offending\n// text, so it belongs on the side that survives.\nfunc boardActCode(verb string, rowID uint64, categoryCode string) string {\n\tcode := verb + \"#\" + strconv.FormatUint(rowID, 10)\n\tif categoryCode != \"\" {\n\t\tcode += \":\" + categoryCode\n\t}\n\treturn code\n}\n\n// The closed editorial set. A switch and not a length check: these are the codes\n// that appear on a person's own page, so the vocabulary is the whole guarantee.\nconst (\n\tboardCodeSpam      = \"spam\"\n\tboardCodeOffTopic  = \"off-topic\"\n\tboardCodeAbuse     = \"abuse\"\n\tboardCodeImpersonation = \"impersonation\"\n\tboardCodeIllegal   = \"illegal-referred\"\n)\n\n// mustBoardCode refuses anything outside the set.\n//\n// THE FULL SET MUST EXIST BEFORE DEPLOY. Realms are write-once, so a category\n// nobody anticipated cannot be added later — which is exactly why the LEGAL\n// verbs below do not use this function. Adding a code here is a deploy, and that\n// is the trade: a fixed vocabulary on the acts that name a person, an extensible\n// one on the acts that answer a statute.\nfunc mustBoardCode(code string) {\n\tswitch code {\n\tcase boardCodeSpam, boardCodeOffTopic, boardCodeAbuse,\n\t\tboardCodeImpersonation, boardCodeIllegal:\n\t\treturn\n\t}\n\tpanic(\"kourtv2: a board moderation code is one of: spam, off-topic, abuse, \" +\n\t\t\"impersonation, illegal-referred\")\n}\n\n// BoardCodes is the vocabulary, for a client that has to offer it.\nfunc BoardCodes() string {\n\treturn boardCodeSpam + \",\" + boardCodeOffTopic + \",\" + boardCodeAbuse + \",\" +\n\t\tboardCodeImpersonation + \",\" + boardCodeIllegal\n}\n\n// GlobalHideRow withholds one comment's text on every surface — the deep link\n// included, unlike an editorial hide.\n//\n// SINGLE KEY, for the speed a notice needs, and the same windowed m-of-n re-set\n// GlobalHide carries: re-setting a redaction that a global act cleared inside the\n// counter-notice window takes m-of-n, so one rogue key cannot win the reversal\n// race and camp a row redacted.\nfunc GlobalHideRow(cur realm, courtSlug string, claimID, rowID uint64, categoryCode string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tif !isGlobalMod(who) {\n\t\tpanic(\"kourtv2: only a global DAO member may globally hide a comment\")\n\t}\n\tcheckReason(categoryCode)\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tr := mustBoardRow(cs, rowID)\n\tif r.purged {\n\t\t// The bytes are gone and there is no un-purge, so a redaction here cannot\n\t\t// change what anyone sees. Refused rather than accepted-and-inert: it\n\t\t// would otherwise arm the re-set window, record a redaction in the log,\n\t\t// and emit an event, all for an act with no effect. Same rule as the\n\t\t// upvote of a purged row (audit D3).\n\t\tpanic(\"kourtv2: that comment was already destroyed; there is nothing to withhold\")\n\t}\n\tnow := heightNow()\n\t// The SAME §512(g) window the claim-level re-set reads (moderation.gno), on a\n\t// board row instead of a claim, so it shares that gate's predicate rather than\n\t// keeping a second copy of a statutory period.\n\tif !r.global \u0026\u0026 r.globalClearedAt != 0 \u0026\u0026\n\t\treSetWindowOpen(r.globalClearedAtTime, r.globalClearedAt, now) {\n\t\td := ensureGlobalDAO()\n\t\tfire, _ := approveAction(d.pending,\n\t\t\t\"boardreset:\"+c.id+\":\"+strconv.FormatUint(claimID, 10)+\":\"+strconv.FormatUint(rowID, 10),\n\t\t\twho, categoryCode, d.purgeM)\n\t\tif !fire {\n\t\t\treturn\n\t\t}\n\t}\n\tr.global = true\n\t// A redacted row leaves the ranked index: Top must not carry a rank for text\n\t// nobody may read. Newest keeps the stub, so enumeration survives.\n\tscoreIndexDrop(cs, r)\n\t// The log row is written LAST, once the act is certain — same reason\n\t// GlobalHide allocates its claimMod only after the m-of-n gate: a refused\n\t// call must not leave a row saying it happened.\n\tensureClaimMod(c, ensureMod(c), claimID).\n\t\tappendLog(who, boardActCode(\"board-global-hide\", rowID, categoryCode), \"\")\n\temitGlobalAct(c.id, claimID, \"board-global-hide\", who)\n}\n\n// GlobalClearRow is the put-back, and it is the reason the pair exists.\n//\n// §512(g) restores THE SPECIFIC MATERIAL. GlobalClearBoardBits — a court-wide\n// recovery verb — is not that: it answers capture, not a counter-notice. Without\n// this, a wrongly-redacted row could only be restored by clearing every\n// redaction in the court.\nfunc GlobalClearRow(cur realm, courtSlug string, claimID, rowID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tif !isGlobalMod(who) {\n\t\tpanic(\"kourtv2: only a global DAO member may clear a global comment hide\")\n\t}\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tr := mustBoardRow(cs, rowID)\n\tif !r.global {\n\t\tpanic(\"kourtv2: that comment is not globally hidden\")\n\t}\n\tr.global = false\n\tr.globalClearedAt = heightNow() // arms the re-set window\n\tr.globalClearedAtTime = nowTime()\n\tscoreIndexPut(cs, r)\n\tensureClaimMod(c, ensureMod(c), claimID).\n\t\tappendLog(who, boardActCode(\"board-global-clear\", rowID, \"\"), \"\")\n\temitGlobalAct(c.id, claimID, \"board-global-clear\", who)\n}\n\n// PurgeBoardRow destroys one comment's text. m-of-n, global DAO, irreversible.\n//\n// IT ZEROES THE BYTES, and that is the difference from every other purge in this\n// realm. Elsewhere purge sets a tombstone flag and the text stays in state, which\n// is tolerable for a title; a board is an unbounded 2 KB-per-row surface under a\n// runbook that says hosting-is-offence gets a purge within 72 hours, and leaving\n// the bytes there does not discharge that. The row id, its author and its height\n// remain, so the record still says a comment was here and who wrote it.\nfunc PurgeBoardRow(cur realm, courtSlug string, claimID, rowID uint64, categoryCode string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\td := ensureGlobalDAO()\n\tif !d.members.Has(who.String()) {\n\t\t// Verbatim, and check-epoch-coherence arm 14 requires the exact string: every\n\t\t// purge verb in the realm speaks with one voice, so no new one can drift its\n\t\t// own gate past the census.\n\t\tpanic(\"kourtv2: only a global DAO member may purge\")\n\t}\n\tmustCategoryCode(categoryCode)\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tr := mustBoardRow(cs, rowID)\n\tif r.purged {\n\t\t// Already destroyed; a second purge is a no-op, not an error — the exact\n\t\t// rule PurgeModLogRow states for its own log. Checked BEFORE the vote, so\n\t\t// a repeat does not consume an m-of-n proposal or write a second log row\n\t\t// for an act that cannot change anything.\n\t\treturn\n\t}\n\tfire, votedCode := approveAction(d.pending,\n\t\t\"boardpurge:\"+c.id+\":\"+strconv.FormatUint(claimID, 10)+\":\"+strconv.FormatUint(rowID, 10),\n\t\twho, categoryCode, d.purgeM)\n\tif !fire {\n\t\treturn\n\t}\n\tr.purged = true\n\tr.text = \"\" // the bytes, not a flag\n\tscoreIndexDrop(cs, r)\n\tensureClaimMod(c, ensureMod(c), claimID).\n\t\tappendLog(who, boardActCode(\"board-purge\", rowID, votedCode), \"\")\n\temitPurge(c.id, claimID, \"board-row\", who)\n}\n\n// BoardRowState is the row's withholding state as one character, for a client\n// that has to say WHY a comment is not there: \".\" readable, \"h\" hidden by its\n// author or a court mod, \"g\" withheld, \"x\" purged.\nfunc BoardRowState(courtSlug string, claimID uint64, rowID uint64) string {\n\treturn boardMark(mustBoardRow(mustClaim(mustCourt(courtSlug), claimID), rowID))\n}\n\n// maxBatchRows is what one transaction may destroy. The bound is the\n// transaction, not the policy: a batch that does not fit is a batch that fails\n// halfway and leaves nobody able to say which rows went.\nconst maxBatchRows = 64\n\n// PurgeBoardRange destroys the text of a claim's whole comment board, in batches\n// of up to 64 rows per call. Irreversible: the bytes are zeroed, not flagged.\n// Each row's id, author and height survive, so the record still shows that a\n// comment was there and who wrote it.\n//\n// Global DAO members only. categoryCode is the statutory ground for the order.\n//\n// TWO PHASES. The ORDER is agreed once, at the DAO's purge threshold: calls\n// before the threshold is met register a signature and change nothing. Once it\n// fires, the order is recorded — BoardPurgeOrdered reports it immediately — and\n// from then on any single global DAO member may advance the cursor to finish the\n// job, without a fresh agreement per batch.\n//\n// fromRowID is where this batch starts; pass 0 for the first. RETURNS the row id\n// to resume from, or 0 when the board is done. While the threshold is still\n// short it returns `fromRowID` unchanged — nothing has happened yet.\n//\n// Refused when the court or claim does not exist, the caller is not a global DAO\n// member, or categoryCode is not a recognised category.\nfunc PurgeBoardRange(cur realm, courtSlug string, claimID, fromRowID uint64, categoryCode string) uint64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\td := ensureGlobalDAO()\n\tif !d.members.Has(who.String()) {\n\t\tpanic(\"kourtv2: only a global DAO member may purge\")\n\t}\n\tmustCategoryCode(categoryCode)\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tcm := ensureMod(c)\n\tclm := lookupClaimMod(cm, claimID)\n\tif clm == nil || !clm.boardPurgeAll {\n\t\tfire, _ := approveAction(d.pending,\n\t\t\t\"boardpurgeall:\"+c.id+\":\"+strconv.FormatUint(claimID, 10),\n\t\t\twho, categoryCode, d.purgeM)\n\t\tif !fire {\n\t\t\treturn fromRowID\n\t\t}\n\t\tclm = ensureClaimMod(c, cm, claimID)\n\t\tclm.boardPurgeAll = true\n\t\t// The court-level record of the ORDER, written once, so the page can say\n\t\t// a board is under a purge before the last batch has run.\n\t\tclm.appendLog(who, boardActCode(\"board-purge-all\", 0, categoryCode), \"\")\n\t\temitPurge(c.id, claimID, \"board-all\", who)\n\t}\n\tif cs.board == nil {\n\t\treturn 0\n\t}\n\t// THE BOUND COUNTS WORK, NOT VISITS. Counting visits looks equivalent and is\n\t// not: a re-run over an already-purged board would hit the bound at row 65\n\t// and report a resume cursor forever, so \"done\" was unreachable for any board\n\t// over one batch. Skipping a purged row costs a visit and no write, and the\n\t// walk is bounded anyway — maxBoardRowsPerClaim caps a board at 512 rows.\n\tn, next := 0, uint64(0)\n\tcs.board.Iterate(beClaimKey(fromRowID), \"\", func(_ string, v any) bool {\n\t\tr := v.(*boardRow)\n\t\tif r.purged {\n\t\t\treturn false\n\t\t}\n\t\tif n \u003e= maxBatchRows {\n\t\t\tnext = r.id // where the next call resumes; nothing is skipped\n\t\t\treturn true\n\t\t}\n\t\tr.purged = true\n\t\tr.text = \"\" // the bytes, not a flag\n\t\tscoreIndexDrop(cs, r)\n\t\tn++\n\t\treturn false\n\t})\n\treturn next\n}\n\n// BoardPurgeOrdered reports whether a claim's whole board is under a purge order\n// — true from the first batch, so the page does not have to wait for the last.\nfunc BoardPurgeOrdered(courtSlug string, claimID uint64) bool {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn false\n\t}\n\tclm := lookupClaimMod(c.mod, claimID)\n\treturn clm != nil \u0026\u0026 clm.boardPurgeAll\n}\n"},{"name":"boardmod.gno","body":"// The court's own board sanctions (docs/CLAIM_BOARDS.md §7.2, §7.7).\n//\n// FOUR ACTS AND THEIR TWO UNDOS. Hide a row, slash standing, freeze an address\n// out of the board, freeze one claim's board; restore standing, unfreeze. Each\n// sanction takes `speechM`-of-n; each undo takes `requireMod`, not\n// `requireActiveMod`, so a SUSPENDED set can still de-escalate — the same rule\n// UnhideItem follows and for the same reason.\n//\n// NO ELECTION-WINDOW REFUSAL HERE YET, deliberately. §7.3's carve-out rests on\n// two open owner rulings (§7.7): whether \"parties of the contest\" is\n// actor-scoped or target-scoped, and whether a live sanction should lapse at\n// `OpenElection` at all, given that registering a candidate set costs ~0.4 CC\n// net and would clear your own sanction. Shipping either reading now would bake\n// in a capture path. What ships is the half that does not depend on the answer.\n\npackage kourtv2\n\nimport \"strconv\"\n\nconst (\n\t// The ceiling on one freeze, below the 1-day nomination window rather than\n\t// the 7-day vote: the election is the remedy for a capturing set, so a\n\t// freeze must never outlast the chance to nominate against it.\n\tfreezeMaxBlocks = int64(17_280) / 2 // ~12h at 17_280 blocks/day\n\n\t// The gap a court must leave between freezing the same address twice. Without\n\t// it, `freezeMaxBlocks` bounds nothing: a set re-freezes in the next block\n\t// and the ceiling becomes a per-transaction formality.\n\tfreezeGapBlocks = freezeMaxBlocks\n)\n\n// mustBoardModInvariants runs at init over constants only, so it is in no\n// mutation batch and cannot be made vacuous by a test fixture.\nfunc mustBoardModInvariants() {\n\tif freezeMaxBlocks \u003e= 17_280 {\n\t\tpanic(\"kourtv2: a board freeze must expire inside the nomination window\")\n\t}\n\tif freezeGapBlocks \u003c freezeMaxBlocks {\n\t\tpanic(\"kourtv2: the gap between freezes must be at least one freeze long, \" +\n\t\t\t\"or the ceiling is a formality\")\n\t}\n}\n\nfunc init() { mustBoardModInvariants() }\n\n// speechM is the threshold every board sanction fires at.\n//\n//\tspeechM = min(n, max(2, m))\n//\n// Never below the set's own m — a court that chose unanimity does not get a\n// looser rule for speech — and never above n, which is what makes it satisfiable\n// at every configuration the realm permits. On a 1-of-1 court it is 1, and that\n// hands a solo creator no power they lacked: HideItem already fires at cm.m = 1\n// and removes the whole claim, board included.\n//\n// Computed at the act site rather than at deploy because it reads per-court m\n// and n, which no init() over package constants can see.\nfunc speechM(cm *courtMod) int {\n\tm := cm.m\n\tif m \u003c 2 {\n\t\tm = 2\n\t}\n\tif m \u003e cm.n {\n\t\tm = cm.n\n\t}\n\treturn m\n}\n\n// SpeechThreshold is the render/client read of the above.\nfunc SpeechThreshold(courtSlug string) int {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn 0\n\t}\n\treturn speechM(c.mod)\n}\n\n// ---- the sanction epoch ---------------------------------------------------\n\n// boardSanctionEpochOf is what makes a freeze clearable in O(1).\n//\n// A FREEZE CARRIES THE EPOCH IT WAS SET UNDER, and is live only while that\n// number still matches the court's. So `GlobalClearBoardBits` and a mod-set\n// reset each clear every freeze in the court by incrementing one integer,\n// instead of walking a tree of unknown size on the remedy path — which §7.5\n// refuses everywhere else in this design for the same reason.\n//\n// Slashes are NOT covered: a slash is a changed number, not a bit, so undoing it\n// takes the per-address `RestoreStanding` / `GlobalRestoreStanding`.\nfunc boardSanctionEpochOf(c *Court) int64 {\n\tif c.mod == nil {\n\t\treturn 0\n\t}\n\treturn c.mod.boardSanctionEpoch\n}\n\n// boardFreezeActive and boardFreezeGapOpen are the address freeze's two clocks.\n//\n// BOTH ARE STORED FUTURE MOMENTS, so both carry a seconds twin written at the\n// same instant as the height — the kind that cannot be reconstructed later,\n// because the `now` they were derived from is gone.\n//\n// THEY CONVERT TOGETHER because the gap only bounds the freeze if the two are\n// measured the same way. freezeGapBlocks equals freezeMaxBlocks precisely so a\n// set cannot hold an address frozen indefinitely in twelve-hour increments; let\n// one run on the clock and the other on blocks, and on a drifting chain the gap\n// expires while the freeze is still live, which is the stacking the ceiling\n// exists to prevent.\n//\n// The freeze's own duration is CALLER-SUPPLIED in blocks (FreezeBoard takes\n// `blocks`, bounded by freezeMaxBlocks), so it converts at the write site.\n//\n// secs is 0 in both: these stamps ARE the deadline, not an event to add a window\n// to. And note the PRESENCE check stays on frozenUntil in boardFrozen — that\n// zero is \"there is no freeze\", not \"the freeze expired\", which is what makes\n// UnfreezeBoard's single assignment still lift it.\nfunc boardFreezeActive(r *standingRow) bool {\n\tpassed, known := pastDeadline(r.frozenUntilTime, 0)\n\treturn (known \u0026\u0026 !passed) || (!known \u0026\u0026 heightNow() \u003c r.frozenUntil)\n}\n\nfunc boardFreezeGapOpen(r *standingRow) bool {\n\tpassed, known := pastDeadline(r.frozenGapUntilTime, 0)\n\treturn (known \u0026\u0026 !passed) || (!known \u0026\u0026 heightNow() \u003c r.frozenGapUntil)\n}\n\n// boardFrozen reports whether who is frozen out of this court's boards. Expiry\n// is LAZY — nothing sweeps, exactly as every other clock in this realm.\nfunc boardFrozen(c *Court, who address) bool {\n\tr := lookupStanding(c, who)\n\tif r == nil || r.frozenUntil == 0 {\n\t\treturn false\n\t}\n\tif r.frozenEpoch != boardSanctionEpochOf(c) {\n\t\treturn false // cleared wholesale; the stamp is stale\n\t}\n\treturn boardFreezeActive(r)\n}\n\n// boardFrozenAt is WHEN an address freeze lifts — the date the gate reads, with\n// its reference height beside it — or (0, 0). Same relationship to boardFrozen\n// that claimBoardFrozenAt has to claimBoardFrozen.\n//\n// Both, not either: the date is what the realm gates on and what a person can\n// plan around, and the height is the reference (clock.gno's pairing). A freeze\n// set before the stamps has no date, so `at` is 0 and only the height is\n// knowable — the caller shows the block and says so.\nfunc boardFrozenAt(c *Court, who address) (at, atHeight int64) {\n\tif !boardFrozen(c, who) {\n\t\treturn 0, 0\n\t}\n\tr := lookupStanding(c, who)\n\treturn r.frozenUntilTime, r.frozenUntil\n}\n\n// BoardFrozenUntil is the exported read: the date a freeze lifts and the height\n// beside it, or (0, 0).\nfunc BoardFrozenUntil(courtSlug string, who address) (at, atHeight int64) {\n\treturn boardFrozenAt(mustCourt(courtSlug), who)\n}\n\n// claimFreezeActive and claimFreezeGapOpen are the claim-board freeze's two\n// clocks — the exact mirror of boardFreezeActive/boardFreezeGapOpen above, on a\n// claimMod instead of a standingRow. Same reasoning throughout: both are stored\n// future moments, so both carry a twin written at the same instant; and they\n// convert TOGETHER because freezeGapBlocks equals freezeMaxBlocks so that a set\n// cannot re-close a board indefinitely in twelve-hour steps, which is a claim\n// about the two windows' relative lengths and fails if only one moves.\n//\n// Two near-identical pairs rather than one generic pair: the records have no\n// common type, and giving them one purely to share four lines would put a shape\n// in the type system to serve a comment. The mirror is stated instead.\nfunc claimFreezeActive(clm *claimMod) bool {\n\tpassed, known := pastDeadline(clm.boardFrozenUntilTime, 0)\n\treturn (known \u0026\u0026 !passed) || (!known \u0026\u0026 heightNow() \u003c clm.boardFrozenUntil)\n}\n\nfunc claimFreezeGapOpen(clm *claimMod) bool {\n\tpassed, known := pastDeadline(clm.boardFrozenGapUntilTime, 0)\n\treturn (known \u0026\u0026 !passed) || (!known \u0026\u0026 heightNow() \u003c clm.boardFrozenGapUntil)\n}\n\n// claimBoardFrozen reports whether ONE claim's board is closed by a moderator\n// act. Same lazy expiry, same epoch stamp.\nfunc claimBoardFrozen(c *Court, claimID uint64) bool {\n\tif c.mod == nil {\n\t\treturn false\n\t}\n\tclm := lookupClaimMod(c.mod, claimID)\n\tif clm == nil || clm.boardFrozenUntil == 0 {\n\t\treturn false\n\t}\n\tif clm.boardFrozenEpoch != c.mod.boardSanctionEpoch {\n\t\treturn false\n\t}\n\treturn claimFreezeActive(clm)\n}\n\n// claimBoardFrozenAt is the height a claim-board freeze lifts, or 0 — the same\n// question claimBoardFrozen answers as a bool, for callers that must PRINT the\n// deadline. One expression, so the page and the gate cannot disagree about\n// whether a freeze is live.\nfunc claimBoardFrozenAt(c *Court, claimID uint64) (at, atHeight int64) {\n\tif !claimBoardFrozen(c, claimID) {\n\t\treturn 0, 0\n\t}\n\tclm := lookupClaimMod(c.mod, claimID)\n\treturn clm.boardFrozenUntilTime, clm.boardFrozenUntil\n}\n\n// ClaimBoardFrozenUntil is the exported read: the date, with its height beside it.\nfunc ClaimBoardFrozenUntil(courtSlug string, claimID uint64) (at, atHeight int64) {\n\treturn claimBoardFrozenAt(mustCourt(courtSlug), claimID)\n}\n\n// ---- hide a row ------------------------------------------------------------\n\n// HideBoardRow is the court's discovery-only hide of one comment, at speechM.\n//\n// NOT 1-of-n: the realm's own precedent for the identical act on a claim,\n// HideItem, is m-of-n, and a board row is lower-stakes than a claim, not higher.\n// The deep link still serves — this is the same bit an author sets on their own\n// row, so a row hidden here reads exactly as one withdrawn, which is why the\n// LEGAL verbs in boardlegal.gno exist separately.\nfunc HideBoardRow(cur realm, courtSlug string, claimID, rowID uint64, code string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\trequireActiveMod(cm, who)\n\tmustBoardCode(code)\n\tcs := mustClaim(c, claimID)\n\tr := mustBoardRow(cs, rowID)\n\tif r.hiddenByMod {\n\t\tpanic(\"kourtv2: that comment is already hidden\")\n\t}\n\tfire, _ := approveAction(cm.pending,\n\t\t\"boardhide:\"+strconv.FormatUint(claimID, 10)+\":\"+strconv.FormatUint(rowID, 10),\n\t\twho, code, speechM(cm))\n\tif !fire {\n\t\treturn\n\t}\n\tr.hiddenByMod = true\n\tscoreIndexDrop(cs, r)\n\tensureClaimMod(c, cm, claimID).\n\t\tappendLog(who, boardActCode(\"board-hide\", rowID, code), \"\")\n\temitModAct(c.id, claimID, \"board-hide\", who)\n}\n\n// UnhideBoardRow clears it. requireMod, not requireActiveMod: de-escalation must\n// outlive a suspension, or a suspended set's mistakes become permanent.\nfunc UnhideBoardRow(cur realm, courtSlug string, claimID, rowID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\trequireMod(cm, who)\n\tcs := mustClaim(c, claimID)\n\tr := mustBoardRow(cs, rowID)\n\tif !r.hiddenByMod {\n\t\tpanic(\"kourtv2: that comment is not hidden\")\n\t}\n\tfire, _ := approveAction(cm.pending,\n\t\t\"boardunhide:\"+strconv.FormatUint(claimID, 10)+\":\"+strconv.FormatUint(rowID, 10),\n\t\twho, \"\", speechM(cm))\n\tif !fire {\n\t\treturn\n\t}\n\t// The MODERATOR's bit only. scoreIndexPut gates on boardMark, so a row whose\n\t// author has also withdrawn it stays out of the ranked index — an unhide must\n\t// not republish what its author withdrew.\n\tr.hiddenByMod = false\n\tscoreIndexPut(cs, r)\n\tensureClaimMod(c, cm, claimID).\n\t\tappendLog(who, boardActCode(\"board-unhide\", rowID, \"\"), \"\")\n\temitModAct(c.id, claimID, \"board-unhide\", who)\n}\n\n// ---- slash standing --------------------------------------------------------\n\n// SlashStanding ratchets one address down by WHOLE LEVELS.\n//\n// WHOLE LEVELS, NOT A PERCENTAGE. A percentage is a different sanction at every\n// court size and at every point on the ladder — 20% of a top-rung score is\n// several rungs on a small court and none on a large one. A level is the unit\n// the whole design is denominated in: it is what the ladder gates, what the\n// throttle prices, and what the person can see on their own page.\n//\n// It never touches `highWater`, which is what makes the act reversible: a\n// successor set raises `score` back toward that number. The asymmetry is the\n// entire safety argument, since there is no appeals lane for a board sanction.\nfunc SlashStanding(cur realm, courtSlug string, who address, levels int64, code string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tactor := cur.Previous().Address()\n\tif levels \u003c 1 || levels \u003e 3 {\n\t\tpanic(\"kourtv2: a slash is 1, 2 or 3 whole levels\")\n\t}\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\trequireActiveMod(cm, actor)\n\tmustBoardCode(code)\n\tif who == \"\" {\n\t\tpanic(\"kourtv2: no such address\")\n\t}\n\tfire, voted := approveAction(cm.pending,\n\t\t\"slash:\"+who.String()+\":\"+strconv.FormatInt(levels, 10),\n\t\tactor, code, speechM(cm))\n\tif !fire {\n\t\treturn\n\t}\n\tr := getStanding(c, who)\n\ttarget := postLevel(c, who) - levels\n\tif target \u003c 0 {\n\t\ttarget = 0\n\t}\n\t// The score that sits exactly AT the target rung, so the address lands on it\n\t// rather than somewhere inside it. At target 0 that is zero standing.\n\tif want := levelFloor(c, target); r.score \u003e want {\n\t\tr.score = want\n\t}\n\t// A SLASH TO ZERO FORFEITS THE PASS, and without this line the whole sanction\n\t// is defeated for 0.1 CC. postLevel floors a pass-holder at level 1, so an\n\t// address that bought one could be slashed to zero standing and still post at\n\t// entry rate forever — leaving the freeze as the only remaining sanction, and\n\t// that is bounded at 12h with a 12h gap, i.e. a 50% duty cycle no court can\n\t// close. Forfeiture rather than refund is the realm's own rule for a\n\t// misused stake, and the burn already happened: nothing is returned here,\n\t// the entitlement is simply spent.\n\t//\n\t// Only at target ZERO. A slash from 3 to 2 leaves the pass alone, because a\n\t// partial sanction that silently destroyed a purchase would be a bigger act\n\t// than the one the moderators voted for.\n\tif target == 0 {\n\t\tr.passHeld = false\n\t}\n\t// THE VERB IN `code`, THE VOTED CATEGORY IN `reason` — the shape claimMod's\n\t// log already uses, and the reason PurgeCourtLogRow has anything to destroy.\n\t// Folding both into one string left the reason field permanently empty, which\n\t// made that verb's own \"the reason leaves state\" line dead code.\n\tcm.appendCourtLog(actor, \"slash:\"+strconv.FormatInt(levels, 10), voted)\n\temitModAct(c.id, 0, \"slash\", actor)\n}\n\n// levelFloor is the score that just reaches level `lv`. Level 0's floor is 0.\n//\n// ONE READER OF THE LADDER THRESHOLDS BESIDES postLevel, and it must agree with\n// it or a slash to level 1 can land at level 0 or 2. Expressed with the same\n// `sup*bps/10_000` shape for that reason.\nfunc levelFloor(c *Court, lv int64) int64 {\n\tif lv \u003c= 0 {\n\t\treturn 0\n\t}\n\tl := ladderFor(c)\n\t// boardSupply returns 0 for a court with no sealed epoch, and 0*bps/10_000 is\n\t// 0, so every rung is already 0 there. No second guard: it would be one this\n\t// corpus cannot catch, because it cannot change an answer.\n\tsup, _ := boardSupply(c)\n\tswitch lv {\n\tcase 1:\n\t\treturn sup * l.t1Bps / 10_000\n\tcase 2:\n\t\treturn sup * l.t2Bps / 10_000\n\t}\n\treturn sup * l.t3Bps / 10_000\n}\n\n// RestoreStanding raises a score back toward `highWater` and never past it.\n//\n// requireMod, so a suspended set can still repair. And CLAMPED rather than\n// checked: a restore that panicked above the high water would make the caller\n// guess the number, and the whole point of storing it is that nobody has to.\nfunc RestoreStanding(cur realm, courtSlug string, who address) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tactor := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\trequireMod(cm, actor)\n\tr := lookupStanding(c, who)\n\tif r == nil || r.score \u003e= r.highWater {\n\t\tpanic(\"kourtv2: that address has nothing to restore\")\n\t}\n\tfire, _ := approveAction(cm.pending, \"restore:\"+who.String(), actor, \"\",\n\t\tspeechM(cm))\n\tif !fire {\n\t\treturn 0\n\t}\n\tr.score = r.highWater\n\tcm.appendCourtLog(actor, \"restore\", \"\")\n\temitModAct(c.id, 0, \"restore\", actor)\n\treturn r.score\n}\n\n// ---- freeze ----------------------------------------------------------------\n\n// FreezeBoard bars one address from BOARD WRITES ONLY, for a bounded time.\n//\n// THE ALLOWLIST IS EVERYTHING ELSE, and it is explicit because the election is\n// the remedy: a frozen address still stakes, votes, answers, disputes, flags,\n// opens claims, withdraws, draws emission, and takes every election action. A\n// freeze that reached any of those would let a set silence the vote that removes\n// it, which is the one thing this power must never be able to do.\nfunc FreezeBoard(cur realm, courtSlug string, who address, blocks int64, code string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tactor := cur.Previous().Address()\n\tif blocks \u003c 1 || blocks \u003e freezeMaxBlocks {\n\t\tpanic(\"kourtv2: a board freeze is at most freezeMaxBlocks long\")\n\t}\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\trequireActiveMod(cm, actor)\n\tmustBoardCode(code)\n\tif who == \"\" {\n\t\tpanic(\"kourtv2: no such address\")\n\t}\n\tnow := heightNow()\n\t// The GAP, read before the m-of-n so a refused proposal cannot be used to\n\t// probe it. A set that could re-freeze in the next block would make\n\t// freezeMaxBlocks a per-transaction formality rather than a ceiling.\n\t// Read from frozenGapUntil, NOT from frozenUntil and NOT gated on the epoch.\n\t// Both of those were wrong: the undo clears frozenUntil, and the recovery verb\n\t// bumps the epoch, so either one handed the set a fresh window. See the\n\t// field's own note in standing.gno.\n\tif r := lookupStanding(c, who); r != nil \u0026\u0026 boardFreezeGapOpen(r) {\n\t\tpanic(\"kourtv2: this address was frozen too recently; the ceiling is not a formality\")\n\t}\n\tfire, voted := approveAction(cm.pending, \"freeze:\"+who.String(), actor, code,\n\t\tspeechM(cm))\n\tif !fire {\n\t\treturn\n\t}\n\tr := getStanding(c, who)\n\tr.frozenUntil = now + blocks\n\tr.frozenUntilTime = nowTime() + blocksToSecs(blocks)\n\tr.frozenEpoch = boardSanctionEpochOf(c)\n\t// Stamped at the FULL ceiling, not at the requested length: a set that asks\n\t// for one block must not thereby earn a shorter cooldown than one that asks\n\t// for twelve hours, or the gap is bought off with short freezes.\n\tr.frozenGapUntil = now + freezeMaxBlocks + freezeGapBlocks\n\tr.frozenGapUntilTime = nowTime() + blocksToSecs(freezeMaxBlocks+freezeGapBlocks)\n\tcm.appendCourtLog(actor, \"freeze:\"+strconv.FormatInt(blocks, 10), voted)\n\temitModAct(c.id, 0, \"freeze\", actor)\n}\n\n// UnfreezeBoard lifts it early. requireMod: de-escalation survives suspension.\nfunc UnfreezeBoard(cur realm, courtSlug string, who address) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tactor := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\trequireMod(cm, actor)\n\tif !boardFrozen(c, who) {\n\t\tpanic(\"kourtv2: that address is not frozen\")\n\t}\n\tfire, _ := approveAction(cm.pending, \"unfreeze:\"+who.String(), actor, \"\",\n\t\tspeechM(cm))\n\tif !fire {\n\t\treturn\n\t}\n\tlifted := lookupStanding(c, who)\n\tlifted.frozenUntil = 0\n\tlifted.frozenUntilTime = 0\n\tcm.appendCourtLog(actor, \"unfreeze\", \"\")\n\temitModAct(c.id, 0, \"unfreeze\", actor)\n}\n\n// FreezeClaimBoard closes ONE claim's board, auto-expiring. The claim itself\n// stays fully live: answers, stakes, votes and the verdict are untouched.\nfunc FreezeClaimBoard(cur realm, courtSlug string, claimID uint64, blocks int64, code string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tactor := cur.Previous().Address()\n\tif blocks \u003c 1 || blocks \u003e freezeMaxBlocks {\n\t\tpanic(\"kourtv2: a board freeze is at most freezeMaxBlocks long\")\n\t}\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\trequireActiveMod(cm, actor)\n\tmustBoardCode(code)\n\t_ = mustClaim(c, claimID)\n\tnow := heightNow()\n\t// The gap, read before the m-of-n so a refused proposal cannot probe it —\n\t// and stamped at the full ceiling below, so a one-block freeze does not buy\n\t// a one-block cooldown.\n\tif prev := lookupClaimMod(cm, claimID); prev != nil \u0026\u0026 claimFreezeGapOpen(prev) {\n\t\tpanic(\"kourtv2: this board was frozen too recently; the ceiling is not a formality\")\n\t}\n\tfire, _ := approveAction(cm.pending,\n\t\t\"freezeboard:\"+strconv.FormatUint(claimID, 10), actor, code, speechM(cm))\n\tif !fire {\n\t\treturn\n\t}\n\tclm := ensureClaimMod(c, cm, claimID)\n\tclm.boardFrozenUntil = now + blocks\n\tclm.boardFrozenUntilTime = nowTime() + blocksToSecs(blocks)\n\tclm.boardFrozenEpoch = cm.boardSanctionEpoch\n\tclm.boardFrozenGapUntil = now + freezeMaxBlocks + freezeGapBlocks\n\tclm.boardFrozenGapUntilTime = nowTime() + blocksToSecs(freezeMaxBlocks+freezeGapBlocks)\n\tclm.appendLog(actor, \"freezeboard:\"+strconv.FormatInt(blocks, 10)+\":\"+code, \"\")\n\temitModAct(c.id, claimID, \"freezeboard\", actor)\n}\n\n// ThawClaimBoard reopens it early. requireMod.\nfunc ThawClaimBoard(cur realm, courtSlug string, claimID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tactor := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\trequireMod(cm, actor)\n\tif !claimBoardFrozen(c, claimID) {\n\t\tpanic(\"kourtv2: that board is not frozen\")\n\t}\n\tfire, _ := approveAction(cm.pending,\n\t\t\"thawboard:\"+strconv.FormatUint(claimID, 10), actor, \"\", speechM(cm))\n\tif !fire {\n\t\treturn\n\t}\n\tthawed := lookupClaimMod(cm, claimID)\n\tthawed.boardFrozenUntil = 0\n\tthawed.boardFrozenUntilTime = 0\n\tensureClaimMod(c, cm, claimID).appendLog(actor, \"thawboard\", \"\")\n\temitModAct(c.id, claimID, \"thawboard\", actor)\n}\n\n// ---- the global backstop ---------------------------------------------------\n\n// GlobalClearBoardBits clears EVERY live board freeze in a court at once.\n//\n// THE PER-BATCH RECOVERY ClearAnyBit CANNOT REACH. ClearAnyBit is claim-keyed, so\n// against a set that froze two hundred addresses it is two hundred transactions\n// on the remedy path. One integer does it here, which is also why it is O(1)\n// rather than a walk — see boardSanctionEpochOf.\n//\n// Slashes are untouched: those are numbers, not bits, and each takes\n// GlobalRestoreStanding.\nfunc GlobalClearBoardBits(cur realm, courtSlug string) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tif !isGlobalMod(who) {\n\t\tpanic(\"kourtv2: only a global DAO member may clear board bits (recovery)\")\n\t}\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\tcm.boardSanctionEpoch++\n\t// THE COURT'S OWN LOG, not only an event. Every other global verb in this\n\t// lane writes one, and these two — the verbs that UNDO a court's sanctions —\n\t// did not. A reader of /mod saw the slash and the freeze recorded and no\n\t// trace that the platform reversed them: the accusation without the\n\t// acquittal. The log is how curation becomes visible after the fact, and an\n\t// intervention nobody can see is worth less than one nobody made.\n\tcm.appendCourtLog(who, \"global-clear-board-bits\", \"\")\n\temitGlobalAct(c.id, 0, \"clear-board-bits\", who)\n\treturn cm.boardSanctionEpoch\n}\n\n// GlobalRestoreStanding is the same repair as RestoreStanding, reachable when the\n// court's own set is the problem. Clamped to highWater identically — the global\n// DAO gets no power to MINT standing, only to put back what a slash took.\nfunc GlobalRestoreStanding(cur realm, courtSlug string, who address) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tactor := cur.Previous().Address()\n\tif !isGlobalMod(actor) {\n\t\tpanic(\"kourtv2: only a global DAO member may restore standing (recovery)\")\n\t}\n\tc := mustCourt(courtSlug)\n\tr := lookupStanding(c, who)\n\tif r == nil || r.score \u003e= r.highWater {\n\t\tpanic(\"kourtv2: that address has nothing to restore\")\n\t}\n\tr.score = r.highWater\n\tensureMod(c).appendCourtLog(actor, \"global-restore-standing\", \"\")\n\temitGlobalAct(c.id, 0, \"restore-standing\", actor)\n\treturn r.score\n}\n\n// mustBoardWritable is the ONE place the two freezes are read, and every board\n// write goes through it.\n//\n// A SINGLE CHOKEPOINT rather than a pair of checks at each entrypoint: two\n// freezes times two write verbs is four call sites that have to stay in\n// agreement, and the third write verb added later is the one that forgets. The\n// address freeze and the claim-board freeze are separate powers with separate\n// undos, so they are checked in one function rather than merged into one flag.\nfunc mustBoardWritable(c *Court, cs *claimState, who address) {\n\tif claimBoardFrozen(c, cs.id) {\n\t\tpanic(\"kourtv2: this claim's board is frozen by moderators\")\n\t}\n\tif boardFrozen(c, who) {\n\t\tpanic(\"kourtv2: you are frozen out of this court's boards\")\n\t}\n}\n\n// PurgeCourtLogRow tombstones ONE court-level log row's reason.\n//\n// ITS OWN VERB, because the two logs have separate sequences: PurgeModLogRow\n// takes a claim id and a row id, and a court row has no claim. Sharing one verb\n// would mean a sentinel claim id, and a sentinel that means \"not a claim\" on a\n// path that also accepts real claim ids is how the wrong row gets destroyed.\nfunc PurgeCourtLogRow(cur realm, courtSlug string, rowID uint64, categoryCode string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\td := ensureGlobalDAO()\n\tif !d.members.Has(who.String()) {\n\t\tpanic(\"kourtv2: only a global DAO member may purge\")\n\t}\n\tmustCategoryCode(categoryCode)\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\t// Resolve the row BEFORE the vote, so an unreachable id is refused on the\n\t// first approval rather than after m-of-n has been spent on it.\n\tidx := -1\n\tfor i := range cm.log {\n\t\tif cm.log[i].rowID == rowID {\n\t\t\tidx = i\n\t\t\tbreak\n\t\t}\n\t}\n\tif idx \u003c 0 {\n\t\tpanic(\"kourtv2: no such court log row\")\n\t}\n\tif cm.log[idx].purged {\n\t\treturn // already tombstoned; a second purge is a no-op, not an error\n\t}\n\tfire, code := approveAction(d.pending,\n\t\t\"purgecourtrow:\"+c.id+\":\"+strconv.FormatUint(rowID, 10),\n\t\twho, categoryCode, d.purgeM)\n\tif !fire {\n\t\treturn\n\t}\n\tcm.log[idx].purged = true\n\tcm.log[idx].reason = \"\" // gone from state, not merely hidden at render\n\tcm.appendCourtLog(who, \"purgecourtrow:\"+code, \"\")\n\temitPurge(c.id, 0, code, who)\n}\n\n// GlobalClearCourtParams drops BOTH per-court parameter overrides at once.\n//\n// THE BACKSTOP THAT DID NOT EXIST. Every other board power a set holds is\n// reversible by the global DAO — freezes by GlobalClearBoardBits, slashes by\n// GlobalRestoreStanding, hides by GlobalClearRow — which is what S10 requires.\n// The two parameter overrides had no such verb, and their own Clear verbs were\n// reachable only by the set that set them. So a captured set could throttle a\n// whole court's board permanently and the platform's only answer was to suspend\n// or reset the set, neither of which touched the numbers.\n//\n// One verb for both, because it is a recovery hammer rather than a tuning knob,\n// and single-key like the other recovery reads: a redaction that must not be\n// set alone is a different act from an override that must not be LEFT alone.\nfunc GlobalClearCourtParams(cur realm, courtSlug string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tif !isGlobalMod(who) {\n\t\tpanic(\"kourtv2: only a global DAO member may clear court parameters (recovery)\")\n\t}\n\tc := mustCourt(courtSlug)\n\tif c.ladder == nil \u0026\u0026 c.creditRates == nil {\n\t\tpanic(\"kourtv2: this court has no parameter overrides\")\n\t}\n\tc.ladder, c.creditRates = nil, nil\n\tensureMod(c).appendCourtLog(who, \"global-clear-params\", \"\")\n\temitGlobalAct(c.id, 0, \"clear-court-params\", who)\n}\n"},{"name":"buy.gno","body":"// Buy: the one-way bonding curve, V2 disposition — the GNOT is BURNED (PLAN\n// §3.7). No treasury exists, nothing ever redeems, and no \"backing\" figure is\n// read or rendered: CC is participation, not a claim on assets.\n\npackage kourtv2\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime/unsafe\"\n)\n\nconst gnotDenom = \"ugnot\"\n\n// burnSinkPath derives the keyless burn address (pin P14): a sub-realm\n// identity of THIS realm that is never created. Only kourtv2 itself could ever\n// instantiate it (realm.Sub is parent-only), and it never does — so no key,\n// no code, no path to the coins, ever.\nconst burnSinkPath = \"gno.land/r/kourt/kourtv2:burned\"\n\n// Buy mints CC for GNOT on the one-way curve and burns the GNOT. Returns the\n// CC minted; the unspent remainder of the payment is refunded. The curve is\n// the only GNOT-side mint and its position is monotonic — the same stretch is\n// never sold twice.\nfunc Buy(cur realm, slug string) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\t// Payment auth (V1 AUDIT.md obligation, unchanged): a DIRECT user call —\n\t// IsUserCall, not IsUser, so a maketx-run ephemeral realm cannot pre-consume\n\t// the coin envelope. Reading unsafe.OriginSend under IsCurrent+IsUserCall is\n\t// the sanctioned inbound-payment pattern (what AGENTS.md flags is\n\t// unsafe.PreviousRealm, not this).\n\tprev := cur.Previous()\n\tif !prev.IsUserCall() {\n\t\tpanic(\"kourtv2: a buy must be a direct user call\")\n\t}\n\tbuyer := prev.Address()\n\n\tc := mustCourt(slug)\n\tif buyer == cur.Address() {\n\t\tpanic(\"kourtv2: the court's own address may not buy\")\n\t}\n\ttouch(c)\n\n\tsent := unsafe.OriginSend().AmountOf(gnotDenom)\n\tif sent \u003c= 0 {\n\t\tpanic(\"kourtv2: send GNOT to buy the coin\")\n\t}\n\n\tdelta, spent := c.crv.Minted(c.minted, sent)\n\tif delta \u003c= 0 {\n\t\t// Aborting reverts the incoming send — the buyer keeps their GNOT.\n\t\tpanic(\"kourtv2: payment too small to mint a coin\")\n\t}\n\tc.minted += delta // monotonic curve position\n\trecordPrice(c)     // the position moved, so the price may have ticked\n\tc.coin.Mint(buyer, delta)\n\t// Track cumulative GNOT burned into this court and keep the directory's\n\t// burn-ordered index in step. Burned GNOT is the one un-sybil-able signal a\n\t// court can accrue — you cannot fake having burned real money — which is why\n\t// the front page ranks on it (MODERATION.md §3.2). Render-layer only: no\n\t// money path reads the index.\n\treindexBurn(c, spent)\n\t// Accrue this burn toward the buyer's appeals-layer weight (meta.gno). It\n\t// only ACCRUES — nothing is minted until the buyer claims — so meta's supply\n\t// tracks the engaged electorate rather than every passerby.\n\taccrueFranchise(c, buyer, spent)\n\n\t// BURN the spent GNOT: send it to the keyless sink. Verifiable on-chain;\n\t// never described as a scarcity feature (§7.4 / Munchee).\n\tb := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tb.SendCoins(cur.Address(), chain.PackageAddress(burnSinkPath),\n\t\tchain.Coins{chain.NewCoin(gnotDenom, spent)})\n\n\t// THE BUYER'S CHANGE, and it is paid on most buys — which is the opposite of what\n\t// this comment used to say. It read: unreachable at this curve's granularity, delta\n\t// is in base units so one more of them costs far under 1 ugnot, Minted's step-up\n\t// loop plus round-up Cost land `spent` EXACTLY on `sent` for every payment probed,\n\t// and a fixture would have to coarsen the curve to reach it.\n\t//\n\t// Every clause of that is true BELOW d and false above it, and the only payments\n\t// ever probed were below d. A base unit costs s/d: under 1 ugnot while s \u003c d, so the\n\t// round-up cost sequence steps by 0 or 1 and can always finish exactly on the\n\t// payment. Past d it steps by s/d, and a payment lands BETWEEN two reachable totals\n\t// with no way to spend the difference.\n\t//\n\t// Measured on the deployed covid court, which sits at s ≈ 5.1e9 — five times d, so\n\t// one base unit costs 5 ugnot: 100 GNOT buys 19,533,968 of them for 99,999,999, and\n\t// this line returns the last ugnot. The nearest reachable totals either side are\n\t// 99,999,994 and 100,000,004.\n\t//\n\t// Pinned by TestBuyReturnsTheUnspendableRemainder, which builds its payment from\n\t// Cost rather than from Minted so the assertion is not the function under test\n\t// agreeing with itself. It also pins the burn above: sending `sent` instead of\n\t// `spent` over-burns by exactly the ugnot owed back here.\n\tif r := sent - spent; r \u003e 0 {\n\t\tb.SendCoins(cur.Address(), buyer, chain.Coins{chain.NewCoin(gnotDenom, r)})\n\t}\n\treturn delta\n}\n\n// -------------------------------------------------------------------- reads --\n\n// CurvePosition is the monotonic total ever minted by the curve (emission\n// mints separately — see EmittedTotal).\nfunc CurvePosition(slug string) int64 { return mustCourt(slug).minted }\n\n// BurnSink is the keyless address the GNOT goes to — published so anyone can\n// audit the burn against bank balances.\nfunc BurnSink() address { return chain.PackageAddress(burnSinkPath) }\n\n// BurnedGNOT is the sink's live balance — the audit read for the burn.\nfunc BurnedGNOT() int64 {\n\tb := banker.NewReadonlyBanker()\n\treturn b.GetCoins(chain.PackageAddress(burnSinkPath)).AmountOf(gnotDenom)\n}\n\n// CoinBalanceOf is who's court-coin balance (a value read).\nfunc CoinBalanceOf(slug string, who address) int64 { return mustCourt(slug).coin.BalanceOf(who) }\n"},{"name":"claim.gno","body":"// Claims: an immutable statement of fact, a refundable deposit, a conditionally\n// refundable fee, and two stake pools. No shares exist — see stake.gno.\n\npackage kourtv2\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"strings\"\n\n\tcheckpoint \"gno.land/p/kourt/checkpoint/v0\"\n\ttwap \"gno.land/p/kourt/twap/v0\"\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\n// Sides of a claim. A side is an int in the API for gnokey ergonomics.\nconst (\n\tsideYES = 0\n\tsideNO  = 1\n)\n\nconst twapBuckets = 168 // a week of hourly buckets, as V1\n\n// claimState is one claim. Fields group by lifecycle; answer/dispute/quality\n// fields are added by their modules as they land (fresh code, no live state).\n// A body is optional and bounded. 2,000 characters is a couple of paragraphs —\n// enough to say what would settle the question and what it is not asking, and\n// short of the essay that would make every claim page a wall.\nconst maxClaimBodyLen = 2000\n\n// runeLen counts CHARACTERS, which is what every limit in this realm is stated\n// in and what a person can count.\n//\n// len() counts UTF-8 BYTES, and every one of these caps used it. The two agree\n// only for ASCII, so a 200-character Russian claim title — about 370 bytes — was\n// refused with \"a title is at most 200 characters\", about a title that was 200.\n// The surfaces that collect this text count characters too: an \u003cinput\u003e maxLength\n// is UTF-16 code units, so the composer accepted what the chain then rejected.\n// Every non-Latin script hit the wall at roughly half the stated limit, and\n// nobody writing in English could see it.\n//\n// Counting runes is strictly more permissive, so no claim that was valid becomes\n// invalid. The cost is that text can occupy up to four times the bytes, which\n// the author already pays for through the storage deposit.\nfunc runeLen(s string) int {\n\tn := 0\n\tfor range s {\n\t\tn++\n\t}\n\treturn n\n}\n\ntype claimState struct {\n\tid     uint64\n\ttitle  string\n\t// The claim's own statement of what it means: what would settle it, what\n\t// evidence is in view, what it is NOT asking. COURTS_STRUCTURE.md §4 gives\n\t// the rule and the reason — \"the body is append-only, fixed at creation\" —\n\t// and the reason is the attack it prevents: if the body could be revised, an\n\t// author could take a position, watch the market, and rewrite the framing\n\t// until their side is the true one. So there is no editor for it, not even\n\t// inside the polish window where the TITLE can still be corrected. A claim\n\t// filed with a bad body is a claim to close and re-file.\n\tbody   string\n\t// Evidence the claim carries: a sha256 the author committed to and the\n\t// places the bytes may be found. media.gno holds the shape and the reasons;\n\t// docs/CLAIM_MEDIA.md holds the design. Fixed at creation like the body, and\n\t// for the body's reason — an author who could re-point an image could reframe\n\t// the claim after watching the market. A moderator can tombstone a slot.\n\tmedia  []mediaItem\n\tauthor address\n\n\tdeposit  int64 // refundable anti-spam deposit (escrowed)\n\tfee      int64 // 10% of deposit: burned on dead/conclusive-low, else refunded\n\tseeded   bool  // moderator-seeded (deposit/fee waived; author draw = 0, I5)\n\t// noAccuracy: this claim pays no ACCURACY slice — drawWinners is zeroed and\n\t// the points stay unminted, exactly as `seeded` does for the author slice.\n\t// Set at open for a `mod:newset` appeal, whose question (\"should this\n\t// court have a folder called X\") is a proposal rather than a matter of fact:\n\t// paying stakers by conviction for agreeing with a proposal is a vote bought\n\t// with the reward pool, and this product's rule is stakes, never ballots. The\n\t// AUTHOR slice is untouched on purpose — proposing a folder the court adopts\n\t// is work worth paying for, and that lane already exists.\n\tnoAccuracy bool\n\topenedAt int64\n\t// Wall-clock stamps for the published deadlines (clock.gno). Heights above\n\t// and below stay as reference; these decide. Zero on pre-upgrade claims,\n\t// which keep their original height gates.\n\topenedAtTime   int64\n\tansweredAtTime int64\n\tverdictAtTime  int64\n\tescrowUntilAt  int64 // absolute: the reopen window's close, in unix seconds\n\tclosed         bool  // dead-claim close (unanswered past the timeout)\n\t// dustBurns IS GONE, and this is what it was for. It recorded what deposit+fee\n\t// actually burned at a conclusive low, so settleSlash could size a bounty\n\t// top-up against real burns rather than params — a seeded claim burns\n\t// nothing, and the params base would have under-paid the flagger. settleSlash\n\t// and the conclusive-low path both went with the quality lane, leaving the\n\t// declaration as its only non-comment line in the realm.\n\t// Removed together with flagger and flagVoteEnd: all three changed\n\t// claimState's STORED shape, which is why they waited for a reseed rather\n\t// than going out with the code that read them.\n\t// The board (board.gno). Four indexes, nil until the first comment: every\n\t// read tests for nil rather than calling ensure*, because a query carries no\n\t// storage deposit and a lazily-initialising read is unpaid state growth.\n\tboard      *bptree.BPTree // be(rowID)            -\u003e *boardRow\n\tboardTop   *bptree.BPTree // be(rowID)            -\u003e true (top-level only)\n\tboardKids  *bptree.BPTree // be(parent)|be(rowID) -\u003e true\n\tboardWrote *bptree.BPTree // string(addr)         -\u003e int64\n\tboardScore *bptree.BPTree // beInv(score)|beInv(rowID) -\u003e rowID (visible top-level only)\n\tboardVoted *bptree.BPTree // be(rowID)|string(addr)    -\u003e true (one vote per address)\n\tboardNextID uint64\n\n\t// Policing-surface membership (strips.gno, MODERATION.md §5.3). Render-layer\n\t// only: no money path reads them; they make index entry/exit idempotent.\n\tinStrip   bool\n\tinPending bool\n\n\t// The two stake pools and their staker records (stake.gno).\n\tyesStake, noStake int64\n\tstakers           *bptree.BPTree // addr|side -\u003e *stakePos\n\t// Per-side lifetime conviction (rate-weighted, u128) — the reward\n\t// denominators and the manipulation-resistant lifetime ratio (§3.9) —\n\t// and the raw ∫stake·dt pools (the Q7 cap bases), both freeze-capped.\n\tyesConvHi, yesConvLo uint64\n\tnoConvHi, noConvLo   uint64\n\tyesRawHi, yesRawLo   uint64\n\tnoRawHi, noRawLo     uint64\n\t// The side integrals' last-advance markers (advancePools).\n\tyesLastAcc, noLastAcc     int64\n\tyesLastRawAt, noLastRawAt int64\n\n\t// The trailing rings (§3.9): total staked (answerability X̄) and the YES\n\t// pool (the ratio series). Observed on every stake/unstake.\n\toi  twap.Ring // total stake, hourly, one week\n\tyes twap.Ring // YES pool, hourly, one week\n\t// Drawable stake history (stakeseries.gno): the two pools as checkpoint\n\t// series, hourly (trimmed past a week) and daily (kept). VALUE fields on\n\t// this record deliberately — a pointer would be a second object. The twap\n\t// rings above are NOT replaced: they are bounded windows feeding X̄; these\n\t// are the archive a chart reads.\n\tyesH, noH     checkpoint.Series // hourly grain, epoch = height/720 + 1\n\tyesD, noD     checkpoint.Series // daily grain, epoch = height/17280 + 1\n\thistTrimmedTo uint32            // newest hourly epoch a trim rode on (throttle)\n\n\t// Answer-freeze markers (answer.gno sets them; stake/unstake refuse after).\n\t// rateAccAtFreeze pins the era integral at the freeze height so frozen\n\t// conviction can never be repriced by later d_eff moves (audit H1).\n\tfrozenAt        int64 // 0 = live\n\trateAccAtFreeze int64\n\txBarFrozen      int64 // P6: the trailing-X̄ snapshot at the answer\n\t// THE RECORD, per side, as it stood when the claim was answered.\n\t//\n\t// These exist because nothing else preserves it. The conviction integrals are\n\t// add-only and survive forever, but yesStake/noStake are DECREMENTED on every\n\t// withdrawal, so once the participants take their principal home a settled\n\t// claim reads YES 0 / NO 0 — and the docket was rendering that live figure. The\n\t// whitepaper's introduction promises \"every claim, every stake, every verdict,\n\t// every dissent\" stays; the weight did stay, the amounts did not.\n\t//\n\t// xBarFrozen cannot substitute: it is the time-averaged TOTAL, so \"how much was\n\t// on YES when it was answered\" was recoverable from nowhere. Written once,\n\t// beside frozenAt, never touched again.\n\tyesStakeAtFreeze, noStakeAtFreeze int64\n\t// The answer (answer.gno). answerBond is the LIVE escrowed remainder\n\t// (zeroed as disposed, exactly once); answerBond0 is the posted magnitude —\n\t// the anchor the dispute-bond and comp formulas read across rounds.\n\tanswer       int8 // sideYES/sideNO once frozenAt != 0\n\tanswerer     address\n\tanswerBond   int64\n\tanswerBond0  int64\n\tanswerHeight int64\n\n\t// VERDICT_FINAL (P4): set by SettleUndisputed, Finalize, or provClose.\n\tverdictAt int64  // 0 = no verdict yet\n\troute     string // \"undisputed\" | \"vote\" | \"closed\"\n\t// tierRef is the court's typical claim size AS OF THIS CLAIM'S ANSWER — the\n\t// size that would have earned exactly 1x here (tier.gno). Frozen beside\n\t// xBarFrozen and for the same reason: the multiplier is the ratio of the\n\t// two, and a live denominator would let whoever calls the permissionless\n\t// OpenRewards choose it. Zero on any claim frozen before this shipped, which\n\t// tierBpsFor reads as par.\n\ttierRef int64\n\n\t// Quality (quality.gno finalizes; mid is the undisputed default).\n\t// The dispute machine (dispute.gno). provisional is the standing verdict\n\t// between a decided round and Finalize (-1 = none); escrowUntil is the\n\t// reopen window's end, set once at the first resolution.\n\tdisputeOpen   bool\n\t// disputeOpenedTime is when the CURRENT round opened, in wall seconds.\n\t// The height is already on chain — the governor records it and Timings\n\t// returns it — but the height alone cannot be dated: the test clock moves\n\t// heights and seconds by independent skews, so deriving one from the other\n\t// puts the marker months from the events beside it. Overwritten by each\n\t// round, like proposalID, because a chart marks the round that is running.\n\tdisputeOpenedTime int64\n\tround         int64\n\tfailedRounds  int64\n\tdecidedRounds int64\n\t// disputer is the CURRENT round's disputer and is overwritten by every\n\t// OpenDispute. Read it only for this round's bond dispositions — never to\n\t// attribute an earlier round's outcome. See overturnBy.\n\tdisputer      address\n\tdisputeBond   int64\n\t// overturnBy is who won the overturn, recorded AT the round that won it\n\t// rather than read back from `disputer` afterwards — the same discipline\n\t// slashFlagger uses, and for the same reason.\n\t//\n\t// THE BUG THIS FIELD EXISTS TO PREVENT, because it was live and measured: a\n\t// failed reopen does NOT reset `provisional` (only the first resolution\n\t// does), so after an overturn a stranger could open a second round, let it\n\t// fail quorum for half a bond, and become `cs.disputer` — collecting the\n\t// standing credit for an overturn they did not win and denying it to the\n\t// address that did. Attribution has to be stamped when the thing happens.\n\toverturnBy address\n\t// overturnBond is that winner's OWN posted bond, captured at the same\n\t// instant. The dispute credit scales each party on their own risk, and the\n\t// disputer's is min(20%·X̄, 40%·answerBond0) — the answer arm always binds,\n\t// so scaling them on answerBond0 instead paid them 2.5x what they staked.\n\t// cs.disputeBond cannot be read back later; it is zeroed on disposal.\n\toverturnBond int64\n\tproposalID    int64\n\tprovisional   int8\n\tescrowUntil   int64\n\tprovClose     bool\n\t// credEligible: some UPHELD round's overturn side carried \u003e= 1/4 of the\n\t// quorum floor — the record credits only for genuinely contested wins\n\t// (audit M2-1: a weightless self-contest must not mint the credential).\n\tcredEligible bool\n\n\t// The quality lane (quality.gno, §10.2 Q2-Q5) HELD TWO FIELDS HERE: flagger,\n\t// the one flag slot per claim, and flagVoteEnd, the close of its court-local\n\t// sealed 3-bucket tally, which rode flags AND dispute rounds. Both are gone\n\t// with the lane. They outlived it only because dropping a persisted field\n\t// changes claimState's stored shape and so needs a fresh chain.\n\t// NOTHING READ EITHER ONE at the point they were removed — verified as\n\t// selectors, not by name: `cs.flagger` and `cs.flagVoteEnd` appeared nowhere\n\t// in the realm or its tests. The name `flagger` does still appear in three\n\t// test files, every time as a LOCAL variable for an address, which is why a\n\t// bare grep made this look six-times referenced when it was zero.\n\t// slotConsumed IS GONE, and so is every reader that made it interesting. The\n\t// field is not declared in this struct any more; what stood here was a\n\t// twenty-two-line enumeration of SEVEN behavioural readers, written because\n\t// three reviewers costing a change to it had each under-counted them, and\n\t// warning that the cheapest-looking fix to any one silently broke the others.\n\t//\n\t// All seven went with the quality lane. Three were quality.gno itself\n\t// (OpenFlag, reaskQualityTally, resolveQualityRide); two were\n\t// SettleUndisputed's tier-clobber guard and slash-reserve retention, and two\n\t// the same pair in Finalize — and stripped of comments neither of those\n\t// functions now contains any tier or slot code at all (see 76b2941, daee87b).\n\t// The two display readers, FlagState and render.gno's, went with them.\n\t//\n\t// It also carried PLAN §13 v0.56's open owner decision — that jobs (1), (5)\n\t// and (7) keyed slash-eligibility to a tally possibly decided at the\n\t// supply-floorLESS demotionBar, so a sub-bar low disarmed the full-bar slash.\n\t// That decision is moot: there is no latch to split and no slash to disarm.\n\t// Recorded rather than dropped, because \"we decided nothing and the question\n\t// went away with the code\" is a different fact from \"we forgot\".\n\t//\n\t// The two fields BELOW are a different matter: flagger and flagVoteEnd are\n\t// still declared and have zero non-comment readers anywhere in the realm.\n\t// Dead persisted state, not a dead comment — removing them changes the stored\n\t// shape and wants a reseed, so it is filed rather than done here.\n\tvoted          *bptree.BPTree // seq|addr -\u003e true (one vote per tally)\n\t// THE SLASH ESCROW (Q5) IS GONE, with the quality lane. What stood here was\n\t// twenty-five lines on slashLevied, slashFlagger and qualityReasked -- three\n\t// one-way exactly-once latches bounding, respectively, slash ORIGINATION, the\n\t// bounty beneficiary, and the dispute ride's tally wipe. None of the three is\n\t// declared any more, and slashSizeFor, settleSlash and CounterFlag, which the\n\t// text reasoned through, have no non-comment line left in this realm either.\n\t// Its closing worry -- that without the ride latch a sock could re-roll the\n\t// slash and then the tier ~3x inside a 3-week escrow, at the price of a dispute\n\t// bond each time -- describes an attack on machinery that no longer exists.\n\t//\n\t// ONE INVARIANT IN IT IS STILL LIVE AND IS KEPT. The old text explained why\n\t// settleSlash must never fall back to cs.flagger: on a ride path that is either\n\t// the zero address, so grc20votes.Mint's mustBeValid PANICS and the entitlement\n\t// is unpayable forever, or a stale flagger from an earlier cycle. The part that\n\t// outlives the slash path is the CONSEQUENCE -- reservedTail is monotone and\n\t// never decremented (court.gno declares it; emission.gno advances it with\n\t// mustAdd), so enqueueing a senior entitlement that can never be paid does not\n\t// merely fail, it permanently shrinks the reservoir for every later junior\n\t// draw. That hazard belongs to enqueueSenior and reservedTail, which are both\n\t// live, and it is written here rather than deleted with the dead latches.\n\t// OpenRewards marker (Q6-Q8): after it, quality is immutable.\n\trewardsOpened bool\n\t// Carrot pull-claim anchors (set at the conclusive tally)\n\t// The draw (openrewards.gno): D's slices, denominators, one-shot flags.\n\tdrawWinners   int64\n\tdrawAuthor    int64\n\tdrawAnswerer  int64\n\twinPoolConvCC int64\n\topenBlocks    int64\n\tcarrotTotal   int64\n\tcarrotPool    int64 // remaining\n\tcarrotChoice  string\n\tcarrotDenom   int64\n\tdecidedPID    int64\n\t// The deciding round's spam tally, frozen beside decidedPID and for exactly\n\t// its reason: cs.proposalID is OVERWRITTEN by every later round, so by\n\t// OpenRewards it may name a round that did not decide anything. The\n\t// governor's counters do survive a ReleaseRoll — Tally reads yes/no/abstain,\n\t// not the roll — so this is not about the figures disappearing; it is about\n\t// reading the WRONG round's figures.\n\t//\n\t// BOTH numbers, not the ratio: a discount whose denominator is gone cannot\n\t// be checked against anything, and a reader given only \"0.8x\" cannot tell a\n\t// 1-of-5 round from a 20-of-100 one.\n\t//\n\t// Zero on any claim decided before this shipped, which the draw reads as\n\t// \"no discount\" rather than as \"discount to nothing\".\n\tspamW      int64\n\tspamTotalW int64\n\t// spamClosed marks a claim the electorate DISCARDED — more than half the\n\t// weight cast called it spam. It is a reason, not a mechanism: the claim\n\t// also carries provClose, so every withdrawal path that already means\n\t// \"everyone takes their principal back\" works unchanged. What this field\n\t// buys is that a page, and a later reader, can tell \"three rounds failed to\n\t// reach quorum\" from \"the court looked and said this was junk\" — two very\n\t// different things that would otherwise be one flag.\n\tspamClosed bool\n\tauthorPaid    bool\n\tanswererPaid  bool\n}\n\n// OpenClaim posts a claim. The opener escrows deposit + fee; the fee's\n// disposition is decided at resolution (refund unless dead or conclusive low).\n// Refused on a whole-court-purged court (§3.2 carve-out #2).\nfunc OpenClaim(cur realm, courtSlug, title string) uint64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tc := mustCourt(courtSlug)\n\tif courtIsPurged(c) {\n\t\tpanic(\"kourtv2: this court is purged; no new claims may be opened\")\n\t}\n\treturn openClaim(c, cur.Previous().Address(), title, \"\", false)\n}\n\n// OpenClaimP posts a claim WITH A BODY. Additive rather than a third parameter on\n// OpenClaim: that one is called by every committed filetest, txtar and scenario in\n// the tree, and widening it would break all of them for a field the caller may\n// legitimately not want. The `P` suffix follows StartCourtP — same entrypoint,\n// one more thing said about it.\n//\n// There is deliberately no OpenClaimSeededP. A seeded claim is a moderator\n// bootstrapping a young court's docket with somebody else's question; writing a\n// body on another author's behalf is a different act from filing one, and it\n// should not be a quiet parameter on an existing waiver path.\n/* OPEN IT WHERE YOU ARE READING. A set page offered \"Open a claim\" and then had\n   to admit \"it opens in \u003ccourt\u003e; a moderator files it here\" — the claim landed\n   loose and somebody with the moderator bit had to file it afterwards, which for\n   a reader browsing a set is the whole point missed.\n   UNGATED NARROWLY, and every word of that is load-bearing:\n     - AT OPEN ONLY. You file the claim you are creating in this transaction.\n       There is no path here to an existing claim, so nobody can file someone\n       else's work into a set of their choosing.\n     - ONE SET. The argument is a single id; a claim that belongs in several\n       still needs a moderator, which keeps ClaimFolders's multi-membership a\n       curated fact rather than an author's assertion.\n     - LIVE SETS ONLY. A retired set is struck from the tree and a purged one is\n       tombstoned; filing into either would put a claim somewhere no reader can\n       reach.\n     - THE CAP STILL BINDS, through the same fileInto the moderator path uses.\n   WHAT IT COSTS AN ABUSER is what a claim costs: the deposit and the fee are\n   charged by openClaim before this runs, so filing is never cheaper than\n   filing. And a moderator still holds RemoveFromFolder, so this widens who may\n   PUT something in a set, not who may keep it there. */\nfunc OpenClaimIn(cur realm, courtSlug string, folderID uint64, title, body string) uint64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tc := mustCourt(courtSlug)\n\tif courtIsPurged(c) {\n\t\tpanic(\"kourtv2: this court is purged; no new claims may be opened\")\n\t}\n\tcm := ensureMod(c)\n\tf := cm.mustFolder(folderID)\n\tif f.retired || f.purged {\n\t\tpanic(\"kourtv2: that set is retired; a claim filed there would be unreachable\")\n\t}\n\twho := cur.Previous().Address()\n\tid := openClaim(c, who, title, body, false)\n\tfileInto(f, id)\n\temitModAct(c.id, id, \"folder-add\", who)\n\treturn id\n}\n\nfunc OpenClaimP(cur realm, courtSlug, title, body string) uint64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tc := mustCourt(courtSlug)\n\tif courtIsPurged(c) {\n\t\tpanic(\"kourtv2: this court is purged; no new claims may be opened\")\n\t}\n\treturn openClaim(c, cur.Previous().Address(), title, body, false)\n}\n\n// OpenClaimSeeded posts a claim with the CC deposit and fee WAIVED, for a\n// moderator bootstrapping a young court (MODERATION.md §8). Mod-only; a\n// suspended set is refused. The GNOT storage deposit is still the mod's to pay,\n// and the author emission slice never mints on a seeded claim (I5, open the rewards)\n// so the waiver cannot be farmed. Refused on a purged court.\nfunc OpenClaimSeeded(cur realm, courtSlug, title string) uint64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tif courtIsPurged(c) {\n\t\tpanic(\"kourtv2: this court is purged; no new claims may be opened\")\n\t}\n\tcm := ensureMod(c)\n\trequireActiveMod(cm, who) // mod-only; suspension refuses (a read gate, no money)\n\t// A META-INSTALLED set may not seed (setmods vet, 2/3): the waiver skips the\n\t// CC deposit AND fee while the winner/answerer emission slices still mint,\n\t// so seeding is an economic privilege, not a render-layer act. A captured\n\t// meta court seating puppets must not thereby hand them a free claim\n\t// faucet. Creator-appointed and election-installed sets seed normally —\n\t// bootstrapping a young court is the whole point of the verb.\n\tif cm.installedByMeta {\n\t\tpanic(\"kourtv2: a meta-installed moderator set may not seed claims; seed after a local election\")\n\t}\n\treturn openClaim(c, who, title, \"\", true)\n}\n\n// openClaim is the shared creation core. A seeded claim waives deposit+fee.\n// OpenClaimPM posts a claim with a body AND media. Additive for OpenClaimP's\n// reason, restated because it keeps being the right one: OpenClaim is called by\n// every committed filetest, txtar and scenario in the tree, and widening a\n// signature breaks all of them for a field most callers will not want.\n//\n// There is deliberately no OpenClaimSeededPM, following the same rule that\n// refused OpenClaimSeededP. Attaching evidence on another author's behalf is a\n// different act from filing it, and it should not ride in as a quiet parameter\n// on an existing waiver path.\n//\n// The media argument's format is documented on parseMediaArg. A fault in it\n// panics rather than dropping the item, because a claim is filed once and cannot\n// be amended: silently discarding evidence would be discovered by the author\n// only after they had paid the deposit for a claim they can no longer fix.\nfunc OpenClaimPM(cur realm, courtSlug, title, body, media string) uint64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tc := mustCourt(courtSlug)\n\tif courtIsPurged(c) {\n\t\tpanic(\"kourtv2: this court is purged; no new claims may be opened\")\n\t}\n\t// Parsed BEFORE the claim is opened, so a malformed media argument is\n\t// refused before the deposit moves. A panic would revert either way; doing\n\t// the cheap check first means the escrow transfer is never work thrown away.\n\titems := parseMediaArg(media)\n\tid := openClaim(c, cur.Previous().Address(), title, body, false)\n\tif len(items) \u003e 0 {\n\t\tmustClaim(c, id).media = items\n\t}\n\treturn id\n}\n\nfunc openClaim(c *Court, who address, title, body string, seeded bool) uint64 {\n\tif runeLen(title) == 0 || runeLen(title) \u003e 200 {\n\t\tpanic(\"kourtv2: a claim title is 1..200 characters\")\n\t}\n\t// Optional: a claim is a title first and the body earns its bytes. The cap is\n\t// not arbitrary — every byte is storage the author pays a deposit on, and a\n\t// court of record wants the question stated, not an essay attached to it.\n\tif runeLen(body) \u003e maxClaimBodyLen {\n\t\tpanic(\"kourtv2: a claim body is at most \" + strconv.Itoa(maxClaimBodyLen) + \" characters\")\n\t}\n\tvar dep, fee int64\n\tif !seeded {\n\t\tdep = effMinDeposit(c)\n\t\tfee = dep * depositFeeBps / 10000\n\t\tmustSpendable(c, who, dep+fee)\n\t\tc.coin.Transfer(who, c.escrow, dep+fee)\n\t\tc.deposit += dep + fee\n\t}\n\tc.nextID++\n\tid := c.nextID\n\tcs := \u0026claimState{\n\t\tid:           id,\n\t\ttitle:        title,\n\t\tbody:         body,\n\t\tauthor:       who,\n\t\tdeposit:      dep,\n\t\tfee:          fee,\n\t\tseeded:       seeded,\n\t\topenedAt:     heightNow(),\n\t\topenedAtTime: nowTime(),\n\t\tstakers:      bptree.NewBPTree32(),\n\t\toi:           twap.New(epochBlocks, twapBuckets, 8),\n\t\tyes:          twap.New(epochBlocks, twapBuckets, 8),\n\t\tprovisional:  -1, // int8 zero is sideYES — the sentinel must be explicit\n\t}\n\tc.claims.Set(beClaimKey(id), cs)\n\t// A seeded claim is force-visible on the pending list from birth: the set\n\t// that seeds content must not also be able to embargo it (§5.3, M-A18).\n\tif seeded {\n\t\tpendingEnter(c, cs)\n\t}\n\t// On the meta court, store the parsed `mod:` instruction (meta.gno).\n\tonClaimOpened(c, cs)\n\t// Claim-lane event: clients enumerate claims off events (no on-chain\n\t// address→claims index exists; MODERATION.md §2/I3).\n\t//\n\t// NO USER TEXT AT ALL, and the title is excluded DELIBERATELY rather than\n\t// forgotten. Four fields go out — court, claim, author, seeded — and every\n\t// one is an identifier or a flag. A client that wants the title reads it\n\t// from state, where purge can still reach it.\n\t//\n\t// This comment used to say \"no user text beyond the title, which is the\n\t// claim's whole public content\", which described an emit that has never\n\t// existed and read as prior sanction for adding one. Events are unpurgeable\n\t// history, so an editor who added the title on the strength of that\n\t// sentence would put user text somewhere purge cannot retract — defeating\n\t// the thing purge is for. moderation.gno states the rule as I6: events\n\t// carry act metadata and a category/act code only, NEVER\n\t// reason/annotation/title text. This lane is not an exception to it.\n\tchain.Emit(\"ClaimOpened\",\n\t\t\"court\", c.id,\n\t\t\"claim\", strconv.FormatUint(id, 10),\n\t\t\"author\", who.String(),\n\t\t\"seeded\", strconv.FormatBool(seeded),\n\t)\n\treturn id\n}\n\n// EditClaimTitle lets the author fix wording during the polish window, before\n// any stake (MODERATION.md §6). The first stake or the window's close freezes\n// the title forever. (With a 0 window the title is frozen from birth.) On the\n// meta court this re-runs the mod: parser — wired in the meta module.\nfunc EditClaimTitle(cur realm, courtSlug string, claimID uint64, newTitle string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif who != cs.author {\n\t\tpanic(\"kourtv2: only the author may edit the title\")\n\t}\n\tif len(newTitle) == 0 || len(newTitle) \u003e 200 {\n\t\tpanic(\"kourtv2: a claim title is 1..200 characters\")\n\t}\n\tif cs.closed || cs.frozenAt != 0 {\n\t\tpanic(\"kourtv2: the claim is no longer editable\")\n\t}\n\tif passed, known := pastDeadline(cs.openedAtTime, blocksToSecs(c.params.stakeOpenDelayBlocks)); (known \u0026\u0026 passed) ||\n\t\t(!known \u0026\u0026 heightNow() \u003e= cs.openedAt+c.params.stakeOpenDelayBlocks) {\n\t\tpanic(\"kourtv2: the polish window has closed; the title is frozen\")\n\t}\n\tif cs.yesStake != 0 || cs.noStake != 0 {\n\t\tpanic(\"kourtv2: staking has begun; the title is frozen\")\n\t}\n\tcs.title = newTitle\n\tonTitleEdited(c, cs) // meta-parser re-run hook (no-op until the meta module)\n\tchain.Emit(\"ClaimTitleEdited\", \"court\", c.id, \"claim\", strconv.FormatUint(claimID, 10))\n}\n\n// CloseDeadClaim closes a claim that never got an answer within the timeout:\n// the deposit refunds to the author, the fee burns (the dead-claim spam price),\n// and new stakes are refused. Existing stakes were never locked (no freeze\n// happened) and remain withdrawable through Unstake. Permissionless.\nfunc CloseDeadClaim(cur realm, courtSlug string, claimID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif cs.closed {\n\t\tpanic(\"kourtv2: already closed\")\n\t}\n\tif cs.frozenAt != 0 {\n\t\tpanic(\"kourtv2: this claim has an answer; it settles, it does not die\")\n\t}\n\tif passed, known := pastDeadline(cs.openedAtTime, deadClaimSecs); (known \u0026\u0026 !passed) ||\n\t\t(!known \u0026\u0026 heightNow() \u003c cs.openedAt+deadClaimTimeout) {\n\t\tpanic(\"kourtv2: the dead-claim timeout has not passed\")\n\t}\n\tcs.closed = true\n\t// Terminal: nothing on a dead claim is policeable any more (§5.3 exits).\n\tpendingExit(c, cs)\n\tstripExit(c, cs)\n\treleaseMetaLatchIfMeta(c, cs)\n\t// Exactly-once dispositions: refund the deposit, burn the fee.\n\tif cs.deposit \u003e 0 {\n\t\tc.coin.Transfer(c.escrow, cs.author, cs.deposit)\n\t\tc.deposit -= cs.deposit\n\t\tcs.deposit = 0\n\t}\n\tif cs.fee \u003e 0 {\n\t\tc.coin.Burn(c.escrow, cs.fee)\n\t\tc.deposit -= cs.fee\n\t\tcs.fee = 0\n\t}\n}\n\n// ------------------------------------------------------------------ reads --\n\nfunc ClaimCount(courtSlug string) uint64 { return mustCourt(courtSlug).nextID }\n\n// ClaimTitle goes through the SAME text gate as render: a purged claim returns\n// its tombstone and a globally-redacted one returns nothing. A raw accessor here\n// would defeat purge — the legal-compliance power — with a single qeval.\nfunc ClaimTitle(courtSlug string, claimID uint64) string {\n\tc := mustCourt(courtSlug)\n\treturn claimTitleFor(c, mustClaim(c, claimID))\n}\n// ClaimBody goes through the same moderation gate as the title. A purged or\n// globally-redacted claim must not answer with the text the purge exists to\n// remove — and the body is the bigger surface of the two, so a read that\n// returned cs.body directly would make the whole moderation layer decorative.\nfunc ClaimBody(courtSlug string, claimID uint64) string {\n\tc := mustCourt(courtSlug)\n\treturn claimBodyFor(c, mustClaim(c, claimID))\n}\n// ClaimMedia answers with the claim's evidence as one line of JSON, through the\n// same moderation gate the title and the body go through — for the reason stated\n// on ClaimBody, which applies here with more force: media is the likeliest thing\n// a purge is FOR, so a read that returned cs.media directly would defeat the\n// legal-compliance power with a single qeval.\n//\n// One line, because the overlay's qeval parser splits raw output on newlines\n// before it matches a typed value. See encodeMedia.\nfunc ClaimMedia(courtSlug string, claimID uint64) string {\n\tc := mustCourt(courtSlug)\n\treturn encodeMedia(claimMediaVisible(c, mustClaim(c, claimID)))\n}\n\nfunc ClaimAuthor(courtSlug string, claimID uint64) address {\n\treturn mustClaim(mustCourt(courtSlug), claimID).author\n}\nfunc ClaimClosed(courtSlug string, claimID uint64) bool {\n\treturn mustClaim(mustCourt(courtSlug), claimID).closed\n}\nfunc ClaimSeeded(courtSlug string, claimID uint64) bool {\n\treturn mustClaim(mustCourt(courtSlug), claimID).seeded\n}\n\n// StakePools returns the two live pool totals — the instantaneous ratio's\n// numerator and denominator.\nfunc StakePools(courtSlug string, claimID uint64) (yes, no int64) {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\treturn cs.yesStake, cs.noStake\n}\n\n// TrailingOI is the flash-resistant trailing average of total stake (X̄).\nfunc TrailingOI(courtSlug string, claimID uint64, window int64) (avg int64, mature bool) {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\treturn cs.oi.Average(heightNow(), window)\n}\n\n// PoolConviction is each side's lifetime rate-weighted conviction in CC units —\n// the numerator/denominator of the manipulation-resistant lifetime ratio (§3.9).\n// Exposed so a client can render that ratio series (render shows it, but a\n// structured UI needs it via qeval).\nfunc PoolConviction(courtSlug string, claimID uint64) (yes, no int64) {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\treturn convToCC(cs.yesConvHi, cs.yesConvLo), convToCC(cs.noConvHi, cs.noConvLo)\n}\n\n// TrailingYes is the flash-resistant trailing average of the YES pool — the\n// numerator of the trailing-week ratio (TrailingOI is its denominator).\nfunc TrailingYes(courtSlug string, claimID uint64, window int64) (avg int64, mature bool) {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\treturn cs.yes.Average(heightNow(), window)\n}\n\n// DocketSeries answers, in ONE query, the four reads a docket row needs for each\n// of many claims:\n//\n//\t\"\u003cid\u003e:\u003cyesStake\u003e:\u003cnoStake\u003e:\u003cconvYes\u003e:\u003cconvNo\u003e:\u003ctrailOI\u003e:\u003coiMature\u003e:\u003ctrailYes\u003e:\u003cyesMature\u003e;…\"\n//\n// mature flags are \"1\" or \"0\". ids is a comma-separated list, in any order; the\n// reply keeps that order and SKIPS an id this court does not have, so a caller\n// matches on the id field rather than on position. At most seriesPageMax ids are\n// answered and the rest are dropped — read the length of what came back.\n//\n// WHY IT EXISTS. The overlay drew a docket of fourteen rows by asking\n// StakePools, PoolConviction, TrailingOI and TrailingYes per row: 56 queries,\n// each one reloading this realm at ~60-90ms, which is most of the four seconds a\n// court page spent reading the chain. Every one of those numbers is already in\n// the claimState this function walks once. Batching at the RPC does not help —\n// gno executes a JSON-RPC array serially — so the batching has to happen HERE,\n// where the realm is loaded once for the whole page.\n//\n// The four single-claim readers stay: a claim page wants one row, and an older\n// overlay is still entitled to ask the old way.\nfunc DocketSeries(courtSlug string, ids string, window int64) string {\n\tc := mustCourt(courtSlug)\n\tnow := heightNow()\n\tout := strings.Builder{}\n\tsent := 0\n\tfor _, part := range strings.Split(ids, \",\") {\n\t\tif sent \u003e= seriesPageMax {\n\t\t\tbreak\n\t\t}\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tid, err := strconv.ParseUint(part, 10, 64)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tv := c.claims.Get(beClaimKey(id))\n\t\tif v == nil {\n\t\t\tcontinue // a claim this court does not have is skipped, never guessed\n\t\t}\n\t\tcs := v.(*claimState)\n\t\toi, oiMature := cs.oi.Average(now, window)\n\t\tyes, yesMature := cs.yes.Average(now, window)\n\t\tif sent \u003e 0 {\n\t\t\tout.WriteString(\";\")\n\t\t}\n\t\tout.WriteString(strconv.FormatUint(id, 10))\n\t\tout.WriteString(\":\" + strconv.FormatInt(cs.yesStake, 10))\n\t\tout.WriteString(\":\" + strconv.FormatInt(cs.noStake, 10))\n\t\tout.WriteString(\":\" + strconv.FormatInt(convToCC(cs.yesConvHi, cs.yesConvLo), 10))\n\t\tout.WriteString(\":\" + strconv.FormatInt(convToCC(cs.noConvHi, cs.noConvLo), 10))\n\t\tout.WriteString(\":\" + strconv.FormatInt(oi, 10))\n\t\tout.WriteString(\":\" + boolDigit(oiMature))\n\t\tout.WriteString(\":\" + strconv.FormatInt(yes, 10))\n\t\tout.WriteString(\":\" + boolDigit(yesMature))\n\t\tsent++\n\t}\n\treturn out.String()\n}\n\n// seriesPageMax caps one DocketSeries reply. The overlay pages the docket at 50;\n// this leaves room for that plus the strips above it without a second query.\nconst seriesPageMax = 60\n\nfunc boolDigit(b bool) string {\n\tif b {\n\t\treturn \"1\"\n\t}\n\treturn \"0\"\n}\n\nfunc mustClaim(c *Court, id uint64) *claimState {\n\tv := c.claims.Get(beClaimKey(id))\n\tif v == nil {\n\t\tpanic(\"kourtv2: no such claim\")\n\t}\n\treturn v.(*claimState)\n}\n\n// beClaimKey is big-endian so claim ids iterate in order.\nfunc beClaimKey(id uint64) string {\n\tb := [8]byte{\n\t\tbyte(id \u003e\u003e 56), byte(id \u003e\u003e 48), byte(id \u003e\u003e 40), byte(id \u003e\u003e 32),\n\t\tbyte(id \u003e\u003e 24), byte(id \u003e\u003e 16), byte(id \u003e\u003e 8), byte(id),\n\t}\n\treturn string(b[:])\n}\n"},{"name":"claimhead.gno","body":"// Package-local: the claim page's whole read, in one query.\n//\n// A SEPARATE FILE BECAUSE OF WHERE IT MAY LOOK. claim.gno moves coin, and this\n// realm forbids a money file from reading the comment board — check-nontransferable\n// enforces it, and caught this function there: \"money WRITES to the standing\n// ledger through the four credit hooks and reads nothing back, which is what\n// keeps a settlement independent of a claim's comment section\". ClaimHead reads\n// the board because a claim PAGE shows the board, and it settles nothing. Here\n// it can, and the guard stays true of the file that handles the money.\npackage kourtv2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n// ClaimHead answers, in ONE query, every NUMBER, FLAG and ADDRESS a claim page\n// needs. Tab-separated, in the fixed order listed below:\n//\n//\tyes  no  convYes  convNo  trailOI  oiMature  trailYes  yesMature\n//\tanswered  disputeOpen  settled  provClose  provisional  closed\n//\tanswer  answerBond  settleAt  answerer  answerRecord\n//\tround  disputeBondNext  escrowUntil  quorumFloor  voteCloses\n//\tverdict  route  rewardsOpened  drawW  drawA  drawAns  drawCarrot\n//\tauthor  hidden  redacted  purged  seeded  stakeOpenDelay\n//\tboardSize  boardOpen\n//\n// WHY IT EXISTS. A qeval costs the same whatever it returns: measured against a\n// live node, reading one field takes 270ms, reading eighty-eight across eleven\n// claims takes 310ms. The price is the realm being loaded into the VM before the\n// question is even read — the same ~20M gas floor a one-field WRITE pays. So a\n// claim page asking fifty-three questions paid that toll fifty-three times, and\n// no amount of parallelism, HTTP/2 or caching could touch it. This is the fix\n// that can: ask once.\n//\n// EVERY FIELD IS THE EXISTING READER'S OWN ANSWER. This function calls them\n// rather than reaching into claimState, so it cannot drift from the reads it\n// replaces — the single readers stay, an older overlay keeps working, and\n// claimhead_test compares the two field by field. Once the realm is loaded the\n// calls are AVL lookups, which is why thirty-eight of them cost what one does.\n//\n// NO FREE TEXT. Title, body, media, the timeline and the association lists keep\n// their own reads: a title may contain a tab, a body contains newlines, and a\n// delimited record that can be broken by a caption is worse than a second query.\n// Fields that do not apply are still present and zero, so the record always has\n// the same number of columns and a reader can index it.\nfunc ClaimHead(courtSlug string, claimID uint64, window int64) string {\n\tc := mustCourt(courtSlug)\n\tmustClaim(c, claimID) // refuse a claim this court does not have, as the single reads do\n\n\tb := strings.Builder{}\n\tn := func(v int64) { b.WriteString(strconv.FormatInt(v, 10) + \"\\t\") }\n\ti := func(v int) { b.WriteString(strconv.Itoa(v) + \"\\t\") }\n\tf := func(v bool) { b.WriteString(boolDigit(v) + \"\\t\") }\n\tt := func(v string) { b.WriteString(v + \"\\t\") }\n\n\tyes, no := StakePools(courtSlug, claimID)\n\tcy, cn := PoolConviction(courtSlug, claimID)\n\toi, oiM := TrailingOI(courtSlug, claimID, window)\n\tty, tyM := TrailingYes(courtSlug, claimID, window)\n\tn(yes)\n\tn(no)\n\tn(cy)\n\tn(cn)\n\tn(oi)\n\tf(oiM)\n\tn(ty)\n\tf(tyM)\n\n\tanswered := HasAnswer(courtSlug, claimID)\n\tsettled := Settled(courtSlug, claimID)\n\tdisputeOpen := DisputeOpen(courtSlug, claimID)\n\tprovClose := ProvClose(courtSlug, claimID)\n\tf(answered)\n\tf(disputeOpen)\n\tf(settled)\n\tf(provClose)\n\ti(Provisional(courtSlug, claimID))\n\tf(ClaimClosed(courtSlug, claimID))\n\n\t// The answer block is zero on a claim nobody has answered, rather than absent:\n\t// a record whose width depends on its content is a record a client has to\n\t// parse twice.\n\tif answered {\n\t\twho := Answerer(courtSlug, claimID)\n\t\ti(AnswerVerdict(courtSlug, claimID))\n\t\tn(AnswerBond(courtSlug, claimID))\n\t\tn(SettleDeadline(courtSlug, claimID))\n\t\tt(who.String())\n\t\ti(AnswerRecord(courtSlug, who))\n\t} else {\n\t\ti(-1)\n\t\tn(0)\n\t\tn(0)\n\t\tt(\"\")\n\t\ti(0)\n\t}\n\n\t// The dispute block applies while an answer stands unsettled — the same gate\n\t// the overlay used when it asked for these one at a time.\n\tif answered \u0026\u0026 !settled {\n\t\tn(FailedRounds(courtSlug, claimID))\n\t\tn(DisputeBondNext(courtSlug, claimID))\n\t\tn(EscrowUntil(courtSlug, claimID))\n\t\tif disputeOpen {\n\t\t\tn(QuorumFloorOf(courtSlug, claimID))\n\t\t\tn(DisputeVoteCloses(courtSlug, claimID))\n\t\t} else {\n\t\t\tn(0)\n\t\t\tn(0)\n\t\t}\n\t} else {\n\t\tn(0)\n\t\tn(0)\n\t\tn(0)\n\t\tn(0)\n\t\tn(0)\n\t}\n\n\tif settled \u0026\u0026 !provClose {\n\t\ti(Verdict(courtSlug, claimID))\n\t\tt(VerdictRoute(courtSlug, claimID))\n\t} else {\n\t\ti(-1)\n\t\tt(\"\")\n\t}\n\n\tcryst := RewardsOpened(courtSlug, claimID)\n\tf(cryst)\n\tif cryst {\n\t\tw, au, an, ca := DrawSlices(courtSlug, claimID)\n\t\tn(w)\n\t\tn(au)\n\t\tn(an)\n\t\tn(ca)\n\t} else {\n\t\tn(0)\n\t\tn(0)\n\t\tn(0)\n\t\tn(0)\n\t}\n\n\tt(ClaimAuthor(courtSlug, claimID).String())\n\tf(HiddenFromListing(courtSlug, claimID))\n\tf(TextRedacted(courtSlug, claimID))\n\tf(ClaimPurged(courtSlug, claimID))\n\tf(ClaimSeeded(courtSlug, claimID))\n\tn(CourtStakeOpenDelay(courtSlug))\n\ti(BoardSize(courtSlug, claimID))\n\tf(BoardOpen(courtSlug, claimID))\n\n\treturn strings.TrimSuffix(b.String(), \"\\t\")\n}\n\n// claimHeadFields is how many columns ClaimHead answers with. Stated here so the\n// realm's own test and the overlay's parser can both refuse a record of the\n// wrong width rather than reading a shifted one.\nconst claimHeadFields = 39\n"},{"name":"clock.gno","body":"package kourtv2\n\n// The two clocks, and which one governs what (owner ruling, v0.46).\n//\n// A PUBLISHED DEADLINE — the settle window, the reopen window, the dead-claim\n// timeout, the polish window, the participant-only week on both terminal paths,\n// the qualified-answerer head start — now gates on BLOCK TIME and is stored as\n// a unix timestamp. Heights are kept beside every one of them for reference, but\n// they no longer decide anything a user plans around. The reason is that a\n// height only means a date if the chain's pace never changes, and pace changes:\n// a halt, an upgrade, or a parameter tune silently re-times every deadline\n// denominated in blocks. A date does not move.\n//\n// THE LIST IS LOAD-BEARING, so add to it only as each gate actually converts.\n// It once named the head start while answer.gno still counted blocks, and a\n// wall-clock test planned from a txtar on the strength of it could not be\n// written. An entry here is a promise about what the code does.\n//\n// An ACCOUNTING CLOCK stays on height, deliberately: conviction accrues per\n// block (rate × blocks), emission periods are counted in blocks, the coin's\n// voting snapshots are height epochs, and the stake-history grain buckets\n// heights. Those are not promises to a reader; they are the units the money\n// math is defined in, and re-denominating them would change what claims pay.\n//\n// Block time is proposer-written and therefore nudgeable by a few seconds\n// where height is pure consensus counting. That is the cost of the trade, and\n// it is the right one for a deadline measured in days: seconds of drift at the\n// boundary are invisible, while a pace change is not.\n//\n// PRE-UPGRADE CLAIMS carry zero timestamps. Every gate below falls back to its\n// original height arithmetic when the stamp is missing, so a claim opened\n// before this change keeps exactly the deadline it was opened under.\n\nimport (\n\t\"chain/runtime\"\n\t\"strconv\"\n)\n\n// Durations in seconds, each the wall-clock meaning its block constant was\n// chosen to express (at the 5s cadence those constants assumed).\n// ONLY THE ONES THAT ARE READ, still — a seconds constant nothing reads is worse\n// than absent, because it asserts a deadline is wall-clock when the gate is\n// height. Each arrives in the same commit as the gate that reads it.\nconst (\n\tsettleSecs    = int64(72 * 3600)      // 72h: an undisputed answer settles\n\tdeadClaimSecs = int64(12 * 7 * 86400) // 12 weeks: an unanswered claim dies\n\t// finalizeGraceSecs is the week OpenRewards and Finalize stay\n\t// participant-only. Read by both now — openrewards.gno from verdictAtTime,\n\t// dispute.gno from escrowUntilAt.\n\tfinalizeGraceSecs = int64(7 * 86400)\n\t// priorityWindowSecs is the qualified-answerer head start (answer.gno). It\n\t// is the LAST term of a sum: the head start runs from the earliest moment a\n\t// claim could have been answerable, which is the polish window plus the\n\t// trailing ring's width after opening. Those two are block-denominated —\n\t// one a court parameter, one the ring itself — so they convert at the call\n\t// site through blocksToSecs while this term is a wall-clock promise outright.\n\tpriorityWindowSecs = int64(24 * 3600)\n)\n\n// nowTime is the block's timestamp in unix seconds — through the test-clock\n// shim (testclock.gno), which is inert unless a throwaway chain armed it.\nfunc nowTime() int64 { return tcNow() }\n\n// deadlineTime answers a claim-scoped deadline in wall-clock terms, or 0 when\n// this claim predates the stamps (the caller then keeps its height gate).\nfunc deadlineTime(stamp, secs int64) int64 {\n\tif stamp == 0 {\n\t\treturn 0\n\t}\n\treturn stamp + secs\n}\n\n// pastDeadline reports whether the block clock has passed stamp+secs, and\n// whether the stamp existed at all. A caller with no stamp falls back to the\n// height arithmetic it used before the stamps existed.\nfunc pastDeadline(stamp, secs int64) (passed, known bool) {\n\tif stamp == 0 {\n\t\treturn false, false\n\t}\n\treturn nowTime() \u003e= stamp+secs, true\n}\n\n// blocksToSecs converts a court-configured block window (the polish window is\n// still configured in blocks, being a court parameter) to seconds at the\n// cadence the parameter was written against.\nfunc blocksToSecs(b int64) int64 { return b * 5 }\n\n// heightNow is the reference height recorded beside every stamp.\n// heightNow is the ONLY height read in this realm. Everything accounting —\n// conviction per block, emission periods, vote closes, twap maturity, the\n// quality quiet window — reads it, so a test chain can advance them all\n// together rather than mining. On an ordinary chain tcHeightSkew is zero and\n// this is exactly runtime.ChainHeight().\n//\n// The floor matters for the same reason the time floor does: once sealed, the\n// skew is dropped, and without a floor the chain would report a height BELOW\n// ones it already wrote into claims — reopening every window keyed on a stored\n// height. scripts/check-height-shim.py enforces that nothing bypasses this.\nfunc heightNow() int64 {\n\tvar h int64\n\tif tcArmed {\n\t\th = tcHeightBase + tcHeightSkew // frozen: only AdvanceTestHeight moves it\n\t} else {\n\t\th = runtime.ChainHeight()\n\t\tif h \u003c tcHeightFloor {\n\t\t\th = tcHeightFloor // a sealed test chain never falls below what it showed\n\t\t}\n\t}\n\treturn h\n}\n\nfunc itoa(v int64) string { return strconv.FormatInt(v, 10) }\n\n// ClaimTimeline is one read for a claim's whole clock: what already happened,\n// stamped in wall-clock seconds by the chain itself, and what is still to come.\n//\n//\t\"opened:\u003cunix\u003e:\u003cheight\u003e;answered:\u003cunix\u003e:\u003cheight\u003e;verdict:\u003cunix\u003e:\u003cheight\u003e;\n//\t settle:\u003cunix\u003e:\u003cheight\u003e;reopen:\u003cunix\u003e:\u003cheight\u003e;now:\u003cunix\u003e:\u003cheight\u003e\"\n//\n// Past events carry the block's own timestamp — no arithmetic, nothing\n// derived. Future rows carry the deadline the realm will actually gate on\n// (a timestamp) with its reference height beside it. A field is omitted when\n// it has not happened; a zero unix stamp means the claim predates the stamps\n// and only its height is knowable.\nfunc ClaimTimeline(courtSlug string, claimID uint64) string {\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tout := \"opened:\" + itoa(cs.openedAtTime) + \":\" + itoa(cs.openedAt)\n\tif cs.answerHeight != 0 {\n\t\tout += \";answered:\" + itoa(cs.answeredAtTime) + \":\" + itoa(cs.answerHeight)\n\t\tif cs.verdictAt == 0 \u0026\u0026 !cs.closed {\n\t\t\tout += \";settle:\" + itoa(deadlineTime(cs.answeredAtTime, settleSecs)) +\n\t\t\t\t\":\" + itoa(cs.answerHeight+settleDelay)\n\t\t}\n\t}\n\tif cs.verdictAt != 0 {\n\t\tout += \";verdict:\" + itoa(cs.verdictAtTime) + \":\" + itoa(cs.verdictAt)\n\t}\n\tif cs.escrowUntil != 0 {\n\t\tout += \";reopen:\" + itoa(cs.escrowUntilAt) + \":\" + itoa(cs.escrowUntil)\n\t}\n\t// THE DISPUTE, which the chart could not draw because nothing reported it.\n\t// A reader could see an answer dated years ago and a vote still running with\n\t// nothing between them. The height comes from the governor, which has had it\n\t// all along — DisputeVoteCloses asks for the same four numbers and keeps\n\t// only the deadline.\n\t// Carried here rather than in a reader of its own so the chart gets it in\n\t// the read it already makes, at no extra round trip.\n\tif cs.disputeOpen \u0026\u0026 cs.proposalID != 0 {\n\t\topened, closes, _, _ := c.gov.Timings(cs.proposalID)\n\t\tout += \";dispute:\" + itoa(cs.disputeOpenedTime) + \":\" + itoa(opened)\n\t\t// AND WHEN THE VOTE SHUTS, which is the row a reader plans around and\n\t\t// the one the page had to ESTIMATE: with only a height to work from, a\n\t\t// client projected the date at an assumed cadence, and that projection\n\t\t// is what showed a vote \"closing in ~7 days\" beside an answer dated\n\t\t// five years earlier. The governor gates on the stamp now, so the\n\t\t// deadline is knowable rather than guessable — published here, in the\n\t\t// read the chart already makes, at no extra round trip.\n\t\t_, closesTime := c.gov.TimingsAt(cs.proposalID)\n\t\tout += \";voteclose:\" + itoa(closesTime) + \":\" + itoa(closes)\n\t}\n\tout += \";now:\" + itoa(nowTime()) + \":\" + itoa(heightNow())\n\t// Fabricated, not Active: the marker has to survive sealing, or a seeded\n\t// chain's timeline goes quiet the moment it stops being drivable.\n\tif TestClockFabricated() {\n\t\t// A consumer plotting these dates must be able to tell a test chain\n\t\t// from a real one without reading prose.\n\t\t// seconds:blocks. The second field was a reserved 0 until the override\n\t\t// grew a height half; a consumer that trusted the dates also needs to\n\t\t// know the HEIGHTS were moved, since every block-denominated figure on\n\t\t// the page — conviction, emission, the trailing average — derives from\n\t\t// a block count the scenario supplied.\n\t\tout += \";testclock:\" + itoa(TestClockPeakSkew()) + \":\" + itoa(TestHeightPeakSkew())\n\t}\n\treturn out\n}\n\n// realmClock hands this realm's height to the pure packages that need it.\n// grc20votes quantises vote epochs by it and the governor gates every voting\n// window on it, and neither may import a realm — so the realm supplies the\n// height instead of them reading the chain. Without this a fast-forwarded test\n// chain could age conviction, emission and every deadline EXCEPT the votes,\n// which is the one thing a dispute waits on.\n//\n// Stateless by design: it holds nothing, so persisting it inside the ledger\n// stores a reference and no data.\ntype realmClock struct{}\n\n// Height satisfies grc20votes.Clock and, through the ledger, the governor's\n// Electorate.\nfunc (realmClock) Height() int64 { return heightNow() }\n\n// Now satisfies the same two interfaces with this realm's wall clock, so the\n// governor's voting deadlines move with a fast-forwarded test chain exactly as\n// its heights do. nowTime() is the test-clock shim, not runtime directly.\nfunc (realmClock) Now() int64 { return nowTime() }\n\n// eventHeight is the height an EMITTED EVENT should carry, as a string.\n//\n// It is heightNow() — the realm's own clock, so an event agrees with the state\n// written in the same transaction. On a fabricated chain that disagrees with\n// the height the CHAIN stamps on the transaction, and an indexer joining the\n// two would see one transaction reporting two heights with nothing to tell it\n// which is which. So a fabricated height says so, in the value itself: an\n// ordinary chain emits \"4812\", a seeded one emits \"121861~test\". A consumer\n// that splits on the tilde gets the number; one that does not gets something\n// obviously not a plain height rather than a plausible lie.\nfunc eventHeight() string {\n\th := strconv.FormatInt(heightNow(), 10)\n\tif TestClockFabricated() {\n\t\treturn h + \"~test\"\n\t}\n\treturn h\n}\n"},{"name":"court.gno","body":"// Package kourtv2 is kourt V2: no-loss conviction staking. Each court is\n// its own coin, governor, and one-way bonding curve, with claims resolved by the\n// V1 answer/dispute machine — but the market layer is gone. Stakers back YES or\n// NO directly; losers always withdraw 1×; winners share a bounded, stepped-down\n// emission of new CC weighted by conviction (stake × time) and an adjudicated\n// quality tier. Real money (GNOT) enters once through the curve and is BURNED;\n// no real money ever exits, and no value ever moves between adversaries\n// (\"forfeitures burn; compensation mints\").\npackage kourtv2\n\n// This file is the backbone: Court and Params, the frozen realm constants and\n// their deploy-checked invariants, the directory, and StartCourt. Mechanics land\n// in sibling files (buy, stake, claim, answer, session, dispute, emission,\n// quality, records, render). PLAN.md (v0.32, design-converged) is the spec; the\n// V1 realm at realm/r/kourtv1 is untouched and remains the audited reference.\n\nimport (\n\t\"strings\"\n\n\tcheckpoint \"gno.land/p/kourt/checkpoint/v0\"\n\tcurve \"gno.land/p/kourt/curve/v0\"\n\tgovernor \"gno.land/p/kourt/governor/v0\"\n\tgrc20votes \"gno.land/p/kourt/grc20votes/v0\"\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\nconst errStaleRealm = \"kourtv2: stale realm; call through a crossing function\"\n\n// coinDecimals and epochBlocks fix every court's coin shape, exactly as V1: six\n// decimals, 720 blocks (an hour) per checkpoint epoch.\nconst (\n\tcoinDecimals = 6\n\tepochBlocks  = int64(720)\n\n\t// curveCap is HALF of V1's: emission grows total supply past what the curve\n\t// sold, provably \u003c 1.784× (the geometric sum of the 20%-ceiling budget with\n\t// the amortized step-down), so halving the curve's own cap keeps\n\t// curve-sold × 1.784 under grc20votes.MaxSupply and every governor tally\n\t// overflow-safe. Checked in mustInvariants.\n\tcurveCap   = (int64(9223372036854775807) / grc20votes.Bps) / 2\n\tcurveDenom = int64(1_000_000_000)\n)\n\n// The frozen emission constants (PLAN §3.3/§4, v0.32). No runtime knob exists —\n// the absence of a retune path is load-bearing for every overflow and capture\n// bound in the audit.\nconst (\n\tperiodBlocks = int64(120_960) // 1 week — the emission accounting period\n\t// budgetWeeklyBps: the OWNER's 20%/yr worst-case ceiling, per week, of live\n\t// supply (2000 bps / 52 ≈ 38 bps). The per-period budget is\n\t// B_n = S_live × budgetWeeklyBps/10000 × the amortized step-down.\n\tbudgetWeeklyBps = int64(38)\n\t// stepDownPeriods: the budget multiplies by 2^(−1/stepDownPeriods) each\n\t// period — a 104-week half-life with no cliff to race (implemented as the\n\t// fixed-point factor stepDownNum/stepDownDen per period in emission.gno).\n\tstepDownPeriods = int64(104)\n\trMaxPeriods     = int64(4) // the reservoir banks at most 4 quiet weeks\n\n\t// THE DRAW MULTIPLIER, in bps of 1×, derived from the claim's own size\n\t// (tier.gno). It replaced three voted bands; these three constants replace\n\t// tierLowX/MidX/HighX = 0/1/2.\n\t//\n\t// tierCeilBps IS THE OLD HIGH, unchanged: 2×. The maximum draw per claim,\n\t// and therefore the emission ceiling every deploy invariant is fitted to, is\n\t// exactly where it was. What moved is which claims reach the top — a ⅔ vote\n\t// mandate before, a size now.\n\t//\n\t// tierRefBps IS THE SIZE THAT EARNS PAR, 0.40% of the court's own supply,\n\t// and it is chosen to sit in the middle of the only window where a\n\t// multiplier does anything at all:\n\t//\n\t//   below ordAnswerXFloorBps (10 bps)  the claim cannot be answered, so a\n\t//                                      reference there would never be read\n\t//   above ~100 bps                     `want \u003e curPeriodBudget` clamps the\n\t//                                      draw, so the multiplier is inert —\n\t//                                      the budget is 38 bps of supply and a\n\t//                                      12-week hot claim reaches it there\n\t//\n\t// 40 bps puts par at 4× the answerability floor and leaves the ceiling\n\t// reachable at 80 bps, inside the clamp. The whole usable range is one\n\t// decade wide and is bounded above by an emission clamp rather than by\n\t// anything about claims, which is why par is not a free parameter.\n\t//\n\t// tierFloorBps IS NOT ZERO, deliberately. The old tierLowX was, because a\n\t// VOTE saying \"worthless\" is a judgement that can justify paying nothing.\n\t// Size cannot: a claim that reaches this function already cleared\n\t// effMinAnswerX, which is the same measure certifying it was worth\n\t// resolving. Zero here would have two thresholds on one quantity\n\t// contradicting each other, and would leave an answerer who posted a bond\n\t// drawing nothing on a claim the court let them answer.\n\t// THE VOTED BANDS ARE GONE — tierLowX, tierMidX and tierHighX, 0/1/2. They\n\t// were the quality lane's three buckets, doubling as draw multipliers, and\n\t// the note here said \"they go when the lane does\". The lane went; they\n\t// stayed, because nothing forced the question.\n\t// Nothing in the realm read them at the end: every economic site reads\n\t// tierBpsFor against tierParBps (tier.gno), which is the bps form and the\n\t// only one the money has ever used. Their six remaining references were all\n\t// in tests — three rows asserting the literals 0/1/2, and three helpers\n\t// multiplying a conviction figure by tierMidX, which is 1. Multiplying by\n\t// one is not a use; those call sites now say what they meant without a\n\t// production constant kept alive to spell it.\n\n\t// tierRefFloorBps FLOORS the reference — it is not the reference. The\n\t// reference is the court's own running average claim size (tier.gno); this\n\t// is the smallest it may be, which is what makes a brand-new court behave\n\t// sensibly and what stops a run of dust claims dragging the average down to\n\t// lift everybody's multiplier.\n\t//\n\t// 40 bps, and the number is MEASURED rather than chosen. The suite's own\n\t// fixtures stake 200 CC on a 50,000 CC court — 40 bps — and at a 20 bps\n\t// floor every one of those ordinary claims landed on the 2x ceiling, which\n\t// the draw-cap tests caught by reporting exactly double their expected\n\t// winners' slice. A reference that calls the typical claim exceptional is\n\t// the wrong reference.\n\t//\n\t// It also sits where it has to: 4x ordAnswerXFloorBps, so the smallest\n\t// ANSWERABLE claim (10 bps) lands exactly on the 0.25x multiplier floor and\n\t// no answerable claim is wasted on the flat part of the curve. That\n\t// alignment is tierFloorBps/tierParBps == ordAnswerXFloorBps/tierRefFloorBps\n\t// and it is worth keeping deliberately rather than by luck.\n\ttierRefFloorBps = int64(40)\n\ttierFloorBps    = int64(2500)  // 0.25× — floor of the multiplier itself\n\ttierCeilBps     = int64(20000) // 2× — the old HIGH, unchanged\n\ttierRefDustCC   = int64(1_000_000)\n\n\t// tierEMAN is the running average's memory, in answered claims. Sixteen is\n\t// long enough that one outsized claim moves the reference by ~6% and short\n\t// enough that a court which changes character re-centres within a season of\n\t// claims. It is also the divisor of an accumulator, so it must be a positive\n\t// int; see observeClaimSize for why the accumulator exists at all.\n\ttierEMAN = int64(16)\n\n\t// tierParBps is 1× and is not a policy knob: it is the unit the other three\n\t// are quoted in. Named so the arithmetic reads as a multiplier rather than\n\t// as a bare 10000 that could be mistaken for grc20votes.Bps.\n\ttierParBps = int64(10000)\n\n\t// The split, in points of a claim's draw: winners 80, author 8, answerer 5\n\t// (tier-scaled, from the main reservoir); the voter carrot is 7 points of\n\t// the MID-weight gross — tier-invariant, senior-queued (emission.gno).\n\tsplitWinners, splitAuthor, splitAnswerer, splitCarrot = int64(80), int64(8), int64(5), int64(7)\n)\n\n// The frozen conduct-economics constants (PLAN §3.4/§3.6, v0.25–v0.31).\nconst (\n\t// answer bond = 6%·X̄ (min 1), capped by params, and the collateralization\n\t// floor in PostAnswer raises it toward 1.6× the destroyable draw — which is\n\t// where the real pricing now lives. 600 = bondFloorFlatBps*4/3, HARDCODED rather\n\t// than derived: deriving it turns the bondFloorFlatBps coupling below into `X \u003e X`,\n\t// and TestEconomicConstantsAreTheCalibratedValues is already the alarm that\n\t// fires if bondFloorFlatBps moves. Why not 450: at exactly bondFloorFlatBps the settle\n\t// refund is zero (the whole bond is retained as the slash reserve) and at 449\n\t// the realm refuses to deploy; 600 returns 25% at settle and leaves margin.\n\tanswerBondBps = int64(600)\n\t// dispute bond = min(20%·X̄, 40% × the ACTUAL answer bond), doubling per\n\t// failed round (round-3 fix: keeps the round-1 honest bar at q \u003e 1/3 on\n\t// every claim, including answer-bond-capped courts).\n\tdisputeBondXBps, disputeBondOfAnswerBps = int64(2000), int64(4000)\n\tmaxFailedRounds                         = 3\n\n\t// comp = min(2 × prevailing party's own bond, 80% × the loser's burned\n\t// bond), tier-invariant, senior-queued; the 80% arm FLOORS (v0.20/v0.25).\n\tcompOwnX, compOfBurnBps = int64(2), int64(8000)\n\n\t// THE REALM'S \"A BODY GETS THIS LONG TO DECIDE\" UNIT: 7 days.\n\t//\n\t// Was flagCooldownBlocks, the per-flag cooldown, and it outlived the flag lane\n\t// because two unrelated callers had already borrowed it as a DURATION rather\n\t// than as a flag parameter — moderation's m-of-n approval TTL and the election\n\t// cooldown, each of which says so at its use. Named for the meaning they were\n\t// borrowing instead of the mechanism they were borrowing it from.\n\t//\n\t// NOT periodBlocks, which is the same 120_960 and means the emission\n\t// accounting period. moderation.gno makes exactly this argument against\n\t// coupling to reSetWindowBlocks, and it applies here in the same words: tie\n\t// two unrelated rules to one constant and one of them eventually gets changed\n\t// for the other's reasons.\n\tdecideWindowBlocks = int64(120_960)\n\n\t// THE CARROT CLAMP BASE. Was the flag bond — b₀ = max(1 CC, 2%·X̄) — and the\n\t// per-voter carrot clamp was defined against it as b₀/2 − 1, so that an idle\n\t// sybil voting to farm the carrot netted strictly negative against the bond it\n\t// would have had to post. The flag lane is gone and the clamp is not, so the\n\t// two constants are promoted here under their own name rather than left as\n\t// orphaned flag parameters. Values unchanged: the clamp does not move.\n\tcarrotClampXBps  = int64(200)\n\tcarrotClampMinCC = int64(1_000_000) // 1 CC\n\n\t// THE ANSWER BOND'S CONVICTION FLOOR: max(4.5%·X̄, 1.6× a side's mid draw).\n\t//\n\t// These were bondFloorFlatBps and bondFloorConvBps, sizing the conclusive-low slash, and\n\t// the answer bond was floored at the slash so a levy could never clamp from\n\t// under its own collateral. The slash went with the quality lane; the floor\n\t// stayed, because covering the slash was only one of the three things it did\n\t// and the other two are load-bearing (answer.gno states them at the call):\n\t// it scales the bond with the claim's CONVICTION rather than its stake alone,\n\t// and it is what keeps the dispute bond answerer-independent.\n\t//\n\t// Renamed rather than re-derived, and the arithmetic is bit-identical, so no\n\t// bond anywhere changes price. A floor still named for a penalty that no\n\t// longer exists is how the next reader concludes it is dead and deletes it.\n\tbondFloorFlatBps = int64(450)   // 4.5%·X̄, the base arm\n\t// 1.07× the side's would-be mid draw, so the bond tracks the take. IT SITS ON\n\t// ITS OWN FLOOR: the check below refuses anything under (par + carrot)·100 =\n\t// 10700, because the draw cap is answerBond0 MINUS the carrot, so a bond of\n\t// exactly 1.00× would leave 0.93·mg to cover a 1.00·mg MID payout and cut the\n\t// published rate. There is no room beneath this without moving the payout or\n\t// the carrot with it.\n\t//\n\t// WHAT IT COSTS, stated because it is a real cost and not a free saving. The\n\t// bond is what bounds destroyed/posted for a sniper — dust on one side,\n\t// declared, then 72h of silence — and that ratio is (tier + carrot)/(this/1e4).\n\t// It was 1.07/1.6 = 0.669 at MID and 2.07/1.6 = 1.294 at HIGH; it is now\n\t// 1.00 at MID and 2.07/1.07 = 1.93 at HIGH. In detection terms, a bad answer\n\t// has to be disputed and overturned about two times in three for the attack to\n\t// lose money at HIGH, where 1.6× asked for about four in seven.\n\t//\n\t// The design already accepted leverage above 1 at HIGH — the comment in\n\t// openrewards.gno measured 1.2937 and said sniping an exceptional claim pays —\n\t// so this widens a gap that was open rather than opening one.\n\tbondFloorConvBps = int64(10700)\n\tdepositFeeBps    = int64(1000)  // 10% of the deposit, conditionally refunded\n\n\t// Answer priority (records.gno): difficulty-weighted net record ≥ 3 earns a\n\t// 24h priority window; gate off until 3 addresses qualify; one active\n\t// priority claim per address.\n\tpriorityNetRecord    = 3\n\tpriorityColdStartN   = 3\n\tpriorityWindowBlocks = int64(17_280) // 24h\n\n\tsettleDelay      = int64(51_840)    // 72h before an undisputed answer settles\n\tdeadClaimTimeout = int64(1_451_520) // 12 weeks: unanswered claims close\n\n\t// Moderation build constants (MODERATION.md §9, §2). metaSlug is reserved so\n\t// only realm init can create the meta court; the public StartCourt path\n\t// refuses it. maxStakeOpenDelay caps the polish window at 24h.\n\tmetaSlug = \"meta\"\n\t// Meta's emission is a FIXED ABSOLUTE weekly budget, never S_live × bps\n\t// (§13.8, unanimous). Meta's supply tracks platform-wide burn, so a\n\t// supply-proportional budget would size the appeals court's emission off\n\t// unrelated courts' volume — inflating the prize for junk appeals and\n\t// attaching a platform-growth yield story to the one token that must not\n\t// have one. Absolute keeps the SENIOR lane (comp/bounty/carrot) funded, so\n\t// the deployer's +EV junk-appeal policing survives, while the lifetime bound\n\t// stays provably tiny: 100 CC × Σ2^(−n/104) ≈ 15,000 CC, four orders under\n\t// the tally ceiling.\n\tmetaWeeklyBudget = int64(100_000_000) // 100 CC/week, stepped down\n\t// Meta's answerability and deposit floors scale with ITS OWN supply, because\n\t// that supply grows with platform burn and Params have no retune path after\n\t// deploy: fixed constants would decay to nothing as the platform grew, and\n\t// §5.3's flood-deposit argument would decay with them.\n\t//\n\t// v0.15 (3-agent panel, MODERATION.md §14): the previous 0.01% arm needed\n\t// ~5e8 GNOT of CLAIMED burn to overtake the 100 CC constant, so it never\n\t// bound and both floors were fixed in practice. Worse, the panel found the\n\t// constant is wrong at BOTH ends. A ~500–1,000 GNOT genesis buy puts launch\n\t// meta supply near 1,000 CC, where a 100 CC answerability floor is ~10% of\n\t// supply and the full filing cost (stake + the worst-case bond) is ~15% — so\n\t// FILING AN APPEAL COST 2–3× MORE THAN DECIDING ONE, the same\n\t// incumbency-lock-by-price that mustElectionInvariants already forbids in\n\t// the election lane, unguarded here. The fixed arms are therefore demoted\n\t// to anti-dust floors and the relative arms carry the policy.\n\t// TWO LANES, deliberately not unified (3-agent panel, unanimous). The meta\n\t// pair prices a FRANCHISE-SCOPED act: an appeal's verdict installs or\n\t// suspends moderator sets, so pinning it to the franchise is dimensionally\n\t// right, and meta's claim count is O(1) forever.\n\tanswerXFloorBps = int64(50) // 0.50% of META supply — the audited β from modvote\n\tdepositFloorBps = int64(2)  // 0.02% — sized by §5.3's pending-list flood rule\n\t// The ORDINARY pair is a tenth of it, because an ordinary court's claims are\n\t// the product and the floors SUM. Stake is custodial (Stake transfers into\n\t// escrow), so CC backing claim A cannot back claim B, and each answered\n\t// claim locks the stake plus the bond, worst case (1+maxBondBps/Bps)×floor. Aggregate demand is\n\t// N × 1.5 × bps, a pure number independent of supply, so the fraction is\n\t// what decides how many claims a court may run at once:\n\t//\n\t//\t50 bps → ~40 concurrent answered claims at realistic staking\n\t//\t10 bps → ~200\n\t//\n\t// Meta never feels that because N stays O(1); an ordinary court's N grows\n\t// with its own success. Worse, escrowed CC cannot vote while quorumFloor's\n\t// dominant arm reads RAW supply, so a high floor starves the dispute quorum\n\t// in the same motion — at 50 bps the dispute machine jams near 127 claims.\n\t//\n\t// And the number that settles it: 50 bps overtakes a flat 100 CC at only\n\t// 20,000 CC of supply, so applying meta's fraction here would be MORE\n\t// expensive than the constant this change exists to remove, for every court\n\t// above that. 10 bps reproduces the old 100 CC almost exactly at 100,000 CC\n\t// and fixes both tails.\n\tordAnswerXFloorBps = int64(10) // 0.10% of an ordinary court's own supply\n\tordDepositFloorBps = int64(1)  // 0.01% — §5.3's rule is meta-only; opening a\n\t//                                claim is the product here, so keep the door\n\t//                                a tenth of the answer it opens.\n\tmaxCourtNameLen = 100\n\t// maxSlugLen keeps uppercase(slug) inside GRC20's symbol-length bound.\n\tmaxSlugLen        = 11\n\tmaxStakeOpenDelay = int64(17_280) // 24h\n)\n\n// mustInvariants is the deploy gate for the cross-constant couplings the audit\n// froze (PLAN §10). It runs at init: a build whose constants violate an audited\n// bound refuses to exist rather than shipping a weakened court.\nfunc mustInvariants() {\n\t// Supply ceiling: curve-sold × the 1.784 emission growth bound must stay\n\t// under grc20votes.MaxSupply so yes×Bps never overflows a tally. 1.784 is\n\t// over-approximated as 1785/1000.\n\tmaxSupply := int64(9223372036854775807) / grc20votes.Bps\n\tif curveCap \u003e (maxSupply/1785)*1000 {\n\t\tpanic(\"kourtv2: curveCap × emission growth bound exceeds the tally-safe supply ceiling\")\n\t}\n\t// §13.8 — the meta franchise does NOT need a new supply bound, and this\n\t// assertion is what makes that claim checkable rather than asserted. The\n\t// franchise mint routes through meta's OWN curve, so every unit of meta\n\t// primary issuance is still bounded by curveCap exactly as a purchased unit\n\t// is; the only NEW term is meta's absolute lifetime emission, the geometric\n\t// sum of a stepped-down weekly budget (Σ2^(−n/104) \u003c 151). If a future\n\t// change ever mints the franchise outside the curve, this check is the one\n\t// that must be revisited — and `mustInvariants` would otherwise keep\n\t// certifying a property that had quietly stopped holding.\n\tif metaLifetimeEmission := metaWeeklyBudget * 151; curveCap \u003e maxSupply-metaLifetimeEmission {\n\t\tpanic(\"kourtv2: curveCap + meta's lifetime emission exceeds the tally-safe supply ceiling\")\n\t}\n\t// THE DRAW CAP MUST BE INERT AT THE ORDINARY TIER. openrewards.gno caps a\n\t// claim's draw at answerBond0 minus the carrot, and PostAnswer's floor gives\n\t// answerBond0 \u003e= (bondFloorConvBps/1e4)·mg_max \u003e= (bondFloorConvBps/1e4)·mg_win, so\n\t// capd \u003e= (bondFloorConvBps/1e4 − splitCarrot/100)·mg_win. Requiring that to cover\n\t// tierMidX·mg_win is exactly bondFloorConvBps \u003e= 10700 — and it is the only\n\t// deploy-time certificate that the published MID rate is a promise rather than\n\t// a calibration. Above MID the cap is DESIGNED to bind, which is why the bound\n\t// is tierMidX and not tierHighX.\n\t//\n\t// NOT a leverage bound, and saying so matters. The cap already gives\n\t// destroyed \u003c= answerBond0, so L \u003c= 1 holds at every tier without this line;\n\t// justifying it as \"L \u003c= 1 at MID\" would make it certify something already\n\t// true, which is precisely the defect the check below had for years. Constants\n\t// only, no runtime term — its premises are unconditional facts in answer.gno\n\t// and openrewards.gno, not a falsifiable model. Verified by mutation in this\n\t// form: it fires on bondFloorConvBps AND on splitCarrot.\n\t//\n\t// This replaces a check on answerBondBps, and note it contains no bond term at\n\t// all: with the bond keyed to the destroyable draw, the thing worth certifying\n\t// is the SLASH multiple, not the base.\n\t// ---- the draw multiplier's own parameter relationships (tier.gno) ----\n\t//\n\t// THE ALIGNMENT, and it is the one worth certifying. The multiplier's floor\n\t// must bite exactly where a claim stops being ANSWERABLE:\n\t//\n\t//\ttierFloorBps / tierParBps  ==  ordAnswerXFloorBps / tierRefFloorBps\n\t//\n\t// Left side: the smallest multiplier anything can earn. Right side: the\n\t// smallest claim that can legally be answered, as a fraction of the\n\t// reference that earns par. Equal, and every answerable claim sits on the\n\t// SLOPE — its size actually decides its rate. Unequal in one direction and a\n\t// band of answerable claims is stuck on the flat floor with size making no\n\t// difference to them; in the other, the floor is unreachable and is dead\n\t// code pretending to be a bound.\n\t//\n\t// This held by arithmetic when the constants were chosen and by nothing\n\t// else, which for a relationship spanning two files is how it comes apart:\n\t// ordAnswerXFloorBps is an answerability knob and nobody changing it would\n\t// think to look at a multiplier. Cross-multiplied to keep it in integers.\n\tif tierFloorBps*tierRefFloorBps != tierParBps*ordAnswerXFloorBps {\n\t\tpanic(\"kourtv2: the multiplier's floor no longer meets the answerability floor — \" +\n\t\t\t\"a band of answerable claims would have a rate their size cannot move\")\n\t}\n\t// The ceiling is the old HIGH band, and that is what keeps the maximum draw\n\t// per claim — and with it every emission bound fitted below — exactly where\n\t// it was when the tier was voted. A ceiling above 2x is not a re-keying, it\n\t// is a raise.\n\tif tierCeilBps != 2*tierParBps {\n\t\tpanic(\"kourtv2: the multiplier ceiling is no longer the old HIGH band — \" +\n\t\t\t\"this raises the per-claim draw the emission bounds were fitted to\")\n\t}\n\t// Ordering, and the floor's own job: a reference at or below the\n\t// answerability floor cannot bound anything, because the cheapest claim\n\t// already sits at it.\n\tif tierFloorBps \u003c= 0 || tierFloorBps \u003e= tierParBps || tierParBps \u003e= tierCeilBps {\n\t\tpanic(\"kourtv2: the multiplier clamps are out of order\")\n\t}\n\tif tierRefFloorBps \u003c= ordAnswerXFloorBps {\n\t\tpanic(\"kourtv2: the reference floor must exceed the answerability floor\")\n\t}\n\t// NEGATIVE is what this actually covers, and the `\u003c= 0` is belt over braces.\n\t// Zero cannot reach here: tierEMAN is a typed constant, so `x / tierEMAN`\n\t// with tierEMAN == 0 is a COMPILE error — measured, at tier.gno's tierRefAt\n\t// and observeClaimSize, \"invalid operation: division by zero\". The compiler\n\t// catches that case strictly earlier and harder than a deploy panic could.\n\t// A negative one\n\t// compiles fine and silently inverts the average, which is what is left for\n\t// this line to refuse.\n\tif tierEMAN \u003c= 0 {\n\t\tpanic(\"kourtv2: the running average's memory must be positive\")\n\t}\n\n\t// tierMidX is gone; par is tierParBps and this expression wants it in\n\t// POINTS, the unit splitCarrot is quoted in. Same number, same certificate.\n\tparPoints := tierParBps / 100\n\tif bondFloorConvBps \u003c (parPoints+splitCarrot)*100 {\n\t\tpanic(\"kourtv2: the slash multiple does not cover the mid draw plus the carrot — the draw cap would cut the published MID rate\")\n\t}\n\t// The dispute bond's own-answer arm keeps round 1 at q \u003e 1/3 everywhere:\n\t// 2 × min(20%·X̄, 40%·A) ≤ 80%·A.\n\t//\n\t// THE SHARED answerBondBps FACTOR IS DROPPED, for two reasons. It cancelled,\n\t// so the check was a tautology dressed as an invariant; worse, it could\n\t// TRUNCATE both sides to zero and pass a real violation (at answerBondBps\n\t// \u003c= 1928 an own-answer arm of 4001 breaks the property and the old form did\n\t// not fire).\n\t//\n\t// AND THIS LINE IS NOW LOAD-BEARING. At 5000 bps the two arms of the min TIED\n\t// at 20%·X̄, so the X̄ arm shadowed this property; at 600 the ANSWER arm binds\n\t// on every claim, and nothing else holds the round-1 bar at q \u003e 1/3.\n\tif 2*disputeBondOfAnswerBps \u003e compOfBurnBps {\n\t\tpanic(\"kourtv2: dispute bond arm breaks the round-1 honest-dispute bar\")\n\t}\n\t// 20%/yr ceiling honesty: 52 weeks of the weekly budget may not exceed\n\t// 2000 bps of supply.\n\tif budgetWeeklyBps*52 \u003e 2000 {\n\t\tpanic(\"kourtv2: the weekly budget exceeds the 20%/yr inflation ceiling\")\n\t}\n\t// The split must sum to ≤ 100 tier-scaled points (the carrot is separate,\n\t// mid-weight and senior — see emission.gno).\n\tif splitWinners+splitAuthor+splitAnswerer != 93 {\n\t\tpanic(\"kourtv2: the tier-scaled split must sum to 93 points\")\n\t}\n\t// CALIBRATION, NOT A SAFETY PROOF — read before trusting this (audit round 2).\n\t// A check over CONSTANTS can only make a constant-space claim: on a pool\n\t// roughly CONSTANT over a ≤12-week life, the draw arm k·midGross lands under\n\t// the base bond, i.e. PostAnswer's collateralization floor would stay a NO-OP on\n\t// the honest path. THAT PREMISE IS NOW DELIBERATELY FALSE — the floor binding is\n\t// the design, and the check below asserts it can. Kept as history because the\n\t// reasoning about what a constant-space check can and cannot claim still holds.\n\t// It does NOT prove the\n\t// slash is collateralized. Its premise (midGross/X̄ = rateBpsFP·T_c/1e10, with\n\t// X̄ standing in for the pool) is falsified AT RUNTIME two ways: by a DRAIN —\n\t// Unstake is free and keeps conviction by design (F9), measured at a 224×\n\t// shortfall — and by an over-aged claim, since nothing forced CloseDeadClaim.\n\t// The real guarantee is the runtime floor in PostAnswer, and that part is\n\t// still true: bondFloorAt prices the bond against BOTH sides' conviction and\n\t// PostAnswer takes the larger, with the ≤12-week premise enforced there by the\n\t// dead-claim answerability gate. What is no longer true is the clause about\n\t// pricing \"with the SAME sizer ResolveFlag later draws with\" — the answer-bond\n\t// slash went with the quality lane, and ResolveFlag, slashGrade and settleSlash\n\t// have no non-comment line left in this realm. (SlashStanding is a DIFFERENT\n\t// mechanism and is very much alive: it slashes standing, not a bond.)\n\tmaxRateBpsFP := 255 * (r0WeeklyBps*1_000_000 + budgetBpsFPStart) / 100\n\tmaxTcWeeks := deadClaimTimeout / periodBlocks\n\tmaxMidGrossBps := maxRateBpsFP * maxTcWeeks / 1_000_000\n\t// INVERTED, deliberately, and the direction is the whole change. The floor\n\t// BINDING is now the design — it is what keys the bond to the pool a wrong\n\t// answer could destroy, instead of to a flat fraction of X̄. So this asserts\n\t// the PREMISE still holds: that the draw arm can outgrow the base arm at all.\n\t// It is a premise tripwire rather than a safety bound (it only fires above\n\t// 3083 bps, so a revert to 2000 would pass it), and it is deliberately kept as\n\t// the one remaining answerBondBps coupling so the constant stays greppable.\n\tif bondFloorConvBps*maxMidGrossBps/10000 \u003c= answerBondBps {\n\t\tpanic(\"kourtv2: the draw arm never reaches the base bond — the collateralization floor is inert and the max-keying prices nothing\")\n\t}\n\t// The worst-case bond in bps of X̄. After the floor became the sizer this is\n\t// the DRAW arm, not the base, so anything pricing against \"the bond\" must use\n\t// this and not answerBondBps. Without it the supplyFloor lid below admits\n\t// filing at 6.17% of supply against the 5% turnout bar — a 23.4% overshoot of\n\t// the very incumbency-lock-by-price invariant it exists to hold, reachable on\n\t// a small sealed court with a claim aged toward the hot 12-week extremal. At\n\t// 5000 bps this was impossible because the floor never exceeded the base.\n\tmaxBondBps := bondFloorConvBps * maxMidGrossBps / 10000\n\tif maxBondBps \u003c answerBondBps {\n\t\tmaxBondBps = answerBondBps\n\t}\n\t// The flat arm's twin coupling: the base bond must cover the 4.5%·X̄ arm with\n\t// MARGIN. The bare `bondFloorFlatBps \u003e answerBondBps` form passed at exactly 450 —\n\t// which is precisely the value that zeroes the settle refund, since the whole\n\t// bond is then retained as the slash reserve. The margined form is what makes\n\t// 600 the enforced floor rather than a convention.\n\tif bondFloorFlatBps*4/3 \u003e answerBondBps {\n\t\tpanic(\"kourtv2: the flat slash arm leaves the base answer bond no settle-refund margin\")\n\t}\n\t// The deploy check and the runtime lid must price off the SAME worst case, or\n\t// one of them is wrong. Pin it here rather than duplicating the arithmetic.\n\tif maxAnswerBondBps() != maxBondBps {\n\t\tpanic(\"kourtv2: maxAnswerBondBps drifted from the deploy-time worst-case bond\")\n\t}\n\t// META APPEALS LANE (v0.15). mustElectionInvariants has long carried this\n\t// ratio gate for the election lane; the appeals lane never had one, which is\n\t// how a 100 CC constant came to price filing above deciding at launch scale.\n\t// Filing costs (Bps+answerBondBps)/Bps × X̄, and X̄ floors at answerXFloorBps\n\t// of supply, so the whole filing cost must stay strictly under the 5% bar.\n\t// BOTH lanes, all three checks. A gate that certifies one of two constant\n\t// pairs certifies nothing about the other, and the ordinary pair is the one\n\t// every court uses.\n\tfor _, l := range []struct {\n\t\tname    string\n\t\tfloor   int64\n\t\tdeposit int64\n\t}{\n\t\t{\"meta\", answerXFloorBps, depositFloorBps},\n\t\t{\"ordinary\", ordAnswerXFloorBps, ordDepositFloorBps},\n\t} {\n\t\tfiling := l.floor * (grc20votes.Bps + maxBondBps) / grc20votes.Bps\n\t\tif filing \u003e= quorumSupplyBps {\n\t\t\tpanic(\"kourtv2: the \" + l.name + \" answerability floor prices filing above winning the vote — incumbency lock by price\")\n\t\t}\n\t\tif 50*l.deposit \u003c filing {\n\t\t\tpanic(\"kourtv2: a 50-row flood is cheaper than the self-answer escape from it (§5.3), \" + l.name + \" lane\")\n\t\t}\n\t\tif l.deposit \u003e= l.floor {\n\t\t\tpanic(\"kourtv2: the \" + l.name + \" deposit floor exceeds its answerability floor\")\n\t\t}\n\t}\n}\n\nfunc init() { mustInvariants() }\n\n// Params are the per-court knobs, frozen at StartCourt through mustSane exactly\n// as V1: no retune entrypoint exists.\ntype Params struct {\n\tdisputeThresholdBps  int64 // overturn bar; \u003e 5000 so a tie never overturns\n\tvotingBlocks         int64 // dispute vote length\n\tgraceBlocks          int64 // governor slot-free grace\n\tminAnswerX           int64 // min trailing total stake (CC base) to answer\n\tanswerBondCapCC      int64 // 0 = uncapped (V1 convention)\n\tminClaimDepositCC    int64 // never zero\n\tescrowMinBlocks      int64 // 1 week\n\tescrowMaxBlocks      int64 // 3 weeks\n\tstakeOpenDelayBlocks int64 // polish window: staking closed for this long after OpenClaim (0 = none)\n}\n\nfunc defaultParams() Params {\n\treturn Params{\n\t\tdisputeThresholdBps:  5001,\n\t\tvotingBlocks:         120_960,\n\t\tgraceBlocks:          17_280,\n\t\tminAnswerX:           1_000_000, // 1 CC — a DUST arm; the fraction carries policy\n\t\tanswerBondCapCC:      0,\n\t\tminClaimDepositCC:    1_000_000, // 1 CC\n\t\tescrowMinBlocks:      120_960,\n\t\tescrowMaxBlocks:      362_880,\n\t\tstakeOpenDelayBlocks: 0, // default: no polish window (opt in via StartCourtP)\n\t}\n}\n\n// mustSane refuses params no bound can rescue, V1-style.\nfunc (p Params) mustSane() Params {\n\tif p.disputeThresholdBps \u003c 5001 || p.disputeThresholdBps \u003e 10000 {\n\t\tpanic(\"kourtv2: the overturn threshold must be in [5001, 10000]\")\n\t}\n\tif p.votingBlocks \u003c 1 || p.graceBlocks \u003c 1 {\n\t\tpanic(\"kourtv2: vote and grace windows must be positive\")\n\t}\n\tif p.minAnswerX \u003c 1 {\n\t\tpanic(\"kourtv2: minAnswerX must be positive\")\n\t}\n\tif p.minClaimDepositCC \u003c 1 {\n\t\tpanic(\"kourtv2: the claim deposit must never be zero\")\n\t}\n\tif p.escrowMinBlocks \u003c 1 || p.escrowMaxBlocks \u003c p.escrowMinBlocks {\n\t\tpanic(\"kourtv2: the escrow window is malformed\")\n\t}\n\t// A DEFAULTED VERDICT'S LADDER MUST FIT. ResolveDispute floors the reopen window\n\t// at ladderWindow when the standing verdict is a quorum-less default, and that\n\t// floor is applied with a max() — so it OVERRIDES escrowMaxBlocks, and a ceiling\n\t// below it would be silently ignored rather than respected. Checking the ceiling\n\t// is what gives this a firing input at all: the obvious form, comparing against\n\t// votingBlocks*(maxFailedRounds-2), is UNSATISFIABLE, since ladderWindow exceeds\n\t// that by construction for every graceBlocks \u003e= 1 — swept over 1.8M legal param\n\t// sets and it fires only at maxFailedRounds = 0.\n\t//\n\t// mustSane rather than mustInvariants, and for a sharper reason than \"these are\n\t// all params\": maxFailedRounds is a package CONSTANT, so the relation is two\n\t// params against a constant. mustInvariants sees only constants and no\n\t// params-only gate existed, which is exactly why neither saw this coupling.\n\tif ladderWindow(p) \u003e p.escrowMaxBlocks {\n\t\tpanic(\"kourtv2: the escrow ceiling cannot fit the failed-round ladder a defaulted verdict must walk\")\n\t}\n\tif p.answerBondCapCC \u003c 0 {\n\t\tpanic(\"kourtv2: answerBondCapCC must be non-negative\")\n\t}\n\tif p.stakeOpenDelayBlocks \u003c 0 || p.stakeOpenDelayBlocks \u003e maxStakeOpenDelay {\n\t\tpanic(\"kourtv2: the polish window must be in [0, 24h]\")\n\t}\n\treturn p\n}\n\n// Court is one court instance. Every field is unexported and no method returns\n// a /p/ pointer (borrow rule #2). Emission state lives on the court so each\n// coin's reservoir is isolated with its ledger.\ntype Court struct {\n\t// xBarAcc is tierEMAN × the running average of answered claims' xBarFrozen —\n\t// the court's typical claim size, which tier.gno divides a claim's own size\n\t// by. Held pre-multiplied because the obvious integer EMA truncates its own\n\t// increment to zero and sticks; observeClaimSize has the arithmetic.\n\txBarAcc int64\n\n\tid   string\n\tname string\n\t// One paragraph saying what this court is for and what it will not take.\n\t// Empty on every court that predates it, which reads as \"no description\" —\n\t// the same \"zero on pre-upgrade\" shape claimState.openedAtTime uses, so there\n\t// is no migration and no version field.\n\t//\n\t// EDITABLE, unlike a claim's body, and the asymmetry is the design. A claim's\n\t// body is fixed at creation because a revisable one lets an author take a\n\t// position, watch the market, then rewrite the framing until their side is the\n\t// true one (COURTS_STRUCTURE.md §4) — the body is part of the thing being\n\t// adjudicated. A court description is a signpost over a docket, adjudicates\n\t// nothing, and gates nothing: no money path reads it. When it goes wrong the\n\t// remedy is the one that already exists for bad curation — the moderators who\n\t// wrote it are elected, and replaceable by the same vote.\n\tdesc string\n\t// The court's one picture, on the same terms as its description: curation, on\n\t// the moderator's seat, adjudicating nothing. nil on every court that\n\t// predates it, which reads as \"no image\" — the same \"zero on pre-upgrade\"\n\t// shape desc uses, so there is no migration and no version field.\n\timage *mediaItem\n\ttier  uint8\n\tadmin address // creator; recorded, unused — no retune path exists\n\n\tcoin *grc20votes.Ledger // the court coin (CC)\n\tgov  *governor.Governor // the dispute arbiter; electorate = coin\n\tcrv  curve.Curve        // one-way GNOT -\u003e CC; proceeds are burned\n\n\t// escrow holds deposits and bonds — NOT stake, since v0.34 — and it never\n\t// votes, because those coins are physically here rather than with their\n\t// owners. It holds them because it has to be able to burn or redirect them\n\t// on a loss; that is what makes them custodial and stake not.\n\t//\n\t// It is NOT \"netted out of the quorum floor\", which this comment used to\n\t// claim (v0.29). quorumFloor and qualityBars net the escrow only on their X̄\n\t// arm; the 5%-of-supply arm that usually dominates reads RAW supply, and the\n\t// max() between them escapes the votable/3 ceiling the first arm imposed.\n\t// That sentence is how the arrangement came to be read as a simple oversight\n\t// one lane over from electionFloor, which genuinely does net. Both bars now\n\t// carry a total reachability clamp instead; the netting still does not happen\n\t// and should not be assumed.\n\t//\n\t// STAKE IS NO LONGER HERE (v0.34, lock.gno). It used to be, and it bought\n\t// exactly one thing: disenfranchisement. Stake principal is never slashed and\n\t// never burned — WithdrawStake returns it 1× on both sides — so custody\n\t// secured nothing, while removing the staker's vote COURT-WIDE when the design\n\t// only ever wanted it CLAIM-SCOPED, which the next clause already does\n\t// properly: participants of a claim are excluded from that claim's quality\n\t// lane BY RULE (isParticipant), not by where their coins sit.\n\t//\n\t// Stake is a lock recorded against the holder's own balance now, so the coins\n\t// stay put and keep voting. What custody had been doing invisibly was making\n\t// double-commitment impossible — the ledger refused a second spend of coins\n\t// that had left — so that check now lives in this realm, on every path that\n\t// moves a user's CC in here. See lock.gno; it is the whole risk of the change.\n\tescrow address\n\n\tminted int64 // cumulative curve position (never decremented; prices mints)\n\n\tclaims  *bptree.BPTree // claimID (be64) -\u003e *claimState (claim.gno)\n\tnextID  uint64\n\tparams  Params\n\tdeposit int64 // sum of escrowed deposits+fees, for the conservation checks\n\n\t// Emission state (emission.gno): one cumulative-accrual line carries both\n\t// lanes — seniors reserve intervals on it, juniors draw what lies past the\n\t// senior tail (P5).\n\tcumAccrual      int64 // total budget ever accrued (monotone)\n\taccrualRem      int64 // carried remainder, units of 1/periodBlocks (P10)\n\tcurPeriodBudget int64 // this period's B = S_live × bps at the last roll\n\tcurBudgetBpsFP  int64 // budget CEILING, bps × 1e6 (stepped down at rolls)\n\tdEffBpsFP       int64 // effective d: min(ceiling, realized EMA) — audit-15 P1\n\trateAccFP       int64 // cumulative Σ rate×blocks (bpsFP·block) — the era integral (audit C-H1)\n\temaMinted       int64 // 4-period EMA of minted-per-period\n\temittedAtRoll   int64 // emittedTotal at the last roll\n\tlastAccrual     int64 // height the accrual walk has reached\n\tperiodIndex     int64 // completed periods (step-down n)\n\tcreatedAt       int64 // height at StartCourt\n\treservedTail    int64 // Σ senior amounts ever enqueued (interval tail)\n\tjuniorReserved  int64 // Σ junior reservations (rewardsOpened draws)\n\tseniorOwed      int64 // enqueued − paid\n\tseniorPaid      int64\n\temittedTotal    int64          // every CC ever minted via emission\n\tqueue           *bptree.BPTree // entitlement seq (be64) -\u003e *entitlement\n\tqueueSeq        uint64\n\trecords         *bptree.BPTree // addr -\u003e *answerRecord (records.gno)\n\t// Argument edges (association.gno, COURTS_STRUCTURE.md §5). Two indexes over ONE\n\t// edge object so both directions are a prefix scan. Nil until the first edge\n\t// exists — every read tests for nil rather than allocating, because a query\n\t// carries no storage deposit.\n\tassocOut          *bptree.BPTree // be(from)|be(to) -\u003e *assocEdge\n\tassocIn           *bptree.BPTree // be(to)|be(from) -\u003e the SAME *assocEdge\n\t// Supersedes edges (supersede.gno, docs/SUPERSEDES.md). Same two-index, one\n\t// object shape, and nil for the same reason. Outbound is at most ONE per\n\t// claim — a re-filing re-files one question — so supOf is keyed by `from`\n\t// alone rather than by the pair.\n\tsupOf           *bptree.BPTree // be(from)        -\u003e *supEdge\n\tsupBy           *bptree.BPTree // be(to)|be(from) -\u003e the SAME *supEdge\n\t// locked is addr -\u003e staked CC still sitting in that holder's own balance\n\t// (lock.gno). Created eagerly with the others: it is a money-path read on\n\t// every deposit and bond, and a lazily-built tree would give each of those\n\t// a second branch that has to agree with the write path.\n\tlocked         *bptree.BPTree // addr -\u003e int64 (lock.gno)\n\tvoteLocks      *bptree.BPTree // addr|kind|id|sub -\u003e int64 (votelock.gno)\n\tstakeIdx       *bptree.BPTree // addr|be(claimID/page) -\u003e *stakeRow (stakeindex.gno)\n\tqualifiedCount int            // addresses at score ≥ priorityNetRecord\n\n\t// Moderation state (moderation.gno) — render-layer only; no money path\n\t// reads it. Lazily created on first moderation touch (ensureMod), so a\n\t// court that is never moderated carries a single nil pointer. See\n\t// MODERATION.md §2 (the constitution) and §4 (state homes).\n\tmod *courtMod\n\n\t// Per-claim stake-history pages (stakeseries.gno) — one shared archive for\n\t// all this court's claims, keys \"cs:\u003cid\u003e:yh|nh|yd|nd\". Lazily created on\n\t// the first recorded stake move, so old courts carry a nil until touched.\n\t// Separate from the coin's own archive (inside the Ledger) by construction.\n\tcsArch *checkpoint.Archive\n\n\t// Policing surfaces (strips.gno, MODERATION.md §5.3). Deliberately NOT on\n\t// courtMod: the money path writes them at lifecycle transitions, and going\n\t// through ensureMod would give PostAnswer a moderator-set side effect.\n\t// Lazily created; keyed so ascending iteration IS nearest-deadline-first.\n\tstripIdx   *bptree.BPTree // be(deadline)|be(claimID) -\u003e claimID\n\tpendingIdx *bptree.BPTree // be(openedAt)|be(claimID) -\u003e claimID\n\n\t// Directory ordering (modrender.gno): cumulative GNOT burned into this\n\t// court's curve — the un-sybil-able front-page rank — plus this court's\n\t// current key in the burn index and its creation sequence.\n\tburnedGNOT int64\n\tburnKey    string\n\tcreatedSeq uint64\n\t// burnH checkpoints burnedGNOT so the court page can chart how much real\n\t// value this court has absorbed over time. Render-layer state, like the\n\t// per-claim pool series: no money path reads it.\n\tburnH checkpoint.Series\n\t// priceH checkpoints the coin price. Kept rather than derived because\n\t// price is a function of `minted`, which is not `supply` — supply also\n\t// moves on emissions and burns. Render-layer state, like burnH.\n\tpriceH checkpoint.Series\n\n\t// absBudget: when \u003e 0 this court's weekly emission is a FIXED ABSOLUTE\n\t// amount (stepped down like every other budget) instead of S_live × bps.\n\t// Only the meta court uses it — see metaWeeklyBudget (§13.8).\n\tabsBudget int64\n\n\t// assocBond: this court's override of the realm's default association bond,\n\t// or 0 for \"use the realm default\" (association.gno).\n\t//\n\t// ZERO IS UNSET, NOT FREE, and that is the whole reason for the sentinel.\n\t// Every court that predates this field carries the int64 zero value, so a\n\t// zero meaning \"this court charges nothing\" would silently unprice the\n\t// existing docket — the same \"zero on pre-upgrade reads as absent\" shape\n\t// `desc` above and claimState.openedAtTime use, which is what lets this ship\n\t// with no migration and no version field. A court that wants association to\n\t// be as good as free sets 1.\n\tassocBond int64\n\n\t// ladder overrides the realm-default posting ladder for this court, or nil to\n\t// inherit (posting.gno). A pointer for the reason creditRates is one.\n\tladder *ladder\n\n\t// creditRates overrides the realm-default earning rates for this court, or\n\t// nil to inherit (standing.gno). A POINTER and not a value, because for a\n\t// five-tuple \"all zero\" is a court that deliberately zeroed its board and\n\t// must not read as \"unset\" — the sentinel assocBond can afford with one int.\n\tcreditRates *creditRates\n\n\t// standings is the participation record (standing.gno): addr -\u003e *standingRow.\n\t// Created eagerly with the others rather than lazily, because its writers sit\n\t// on money paths at lifecycle transitions — a lazily-built tree would give\n\t// each of those a second branch that has to agree with the write path, which\n\t// is the shape `locked` is eager to avoid.\n\t//\n\t// NOT `records`, and the two must never be merged. `records` is adjudicated\n\t// and no moderator can touch it; this one is slashable for abuse.\n\tstandings *bptree.BPTree\n}\n\n// courts is the directory: slug -\u003e *Court, lexicographic by slug.\nvar courts = bptree.NewBPTree32()\n\n// StartCourt creates a court: its coin, its governor (electorate = the coin),\n// its curve, and its claim book. Anyone may start one; the directory admin (the\n// first creator) may curate tiers. GNOT handling lives in buy.gno (burned).\nfunc StartCourt(cur realm, slug, name string) string {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\ttakeCourtCreationBurn(cur)\n\treturn startCourtUser(cur.Previous().Address(), cur.Address(), slug, name, 0)\n}\n\n// StartCourtP is StartCourt with an opt-in polish window (§9): staking is\n// closed for stakeOpenDelayBlocks after each OpenClaim, giving the author and\n// moderators a pre-stake window to fix wording before anyone bets. The default\n// StartCourt uses 0 (no window), which preserves every existing caller — so no\n// fixture sweep is forced by adding the field (the audit's concern is moot when\n// the default is 0 and the window is opt-in).\nfunc StartCourtP(cur realm, slug, name string, stakeOpenDelay int64) string {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\ttakeCourtCreationBurn(cur)\n\treturn startCourtUser(cur.Previous().Address(), cur.Address(), slug, name, stakeOpenDelay)\n}\n\n// startCourtUser is the public creation path: it refuses the reserved `meta`\n// slug (only realm init may create the meta court, via startCourt directly),\n// then delegates to startCourt.\nfunc startCourtUser(admin, escrowAddr address, slug, name string, delay int64) string {\n\tif slug == metaSlug {\n\t\tpanic(\"kourtv2: the 'meta' slug is reserved\")\n\t}\n\treturn startCourt(admin, escrowAddr, slug, name, delay)\n}\n\n// startCourt creates a court: its coin, governor, curve, and claim book. It is\n// the reusable core — realm init calls it directly (with the realm address as\n// escrow) to deploy the meta court, which is why it takes admin/escrow as\n// arguments rather than reading a crossing realm. No payment is collected\n// (court-count floods are storage-deposit-priced, MODERATION.md §2/§13.5).\nfunc startCourt(admin, escrowAddr address, slug, name string, delay int64) string {\n\tmustSlug(slug)\n\tmustCourtName(name)\n\tif courts.Has(slug) {\n\t\tpanic(\"kourtv2: that slug is taken\")\n\t}\n\tp := defaultParams()\n\tp.stakeOpenDelayBlocks = delay\n\t// The symbol is DERIVED from the slug, never a shared literal. Every court\n\t// used to mint its ledger as \"COURT\", which grc20votes also adopts as the\n\t// ledger id — so every court's coin was identically labelled AND identically\n\t// identified. Wallets, explorers and indexers key their display on the\n\t// symbol rather than the package path, so a malicious court's coin rendered\n\t// pixel-identical to the flagship's, while the two are non-fungible and\n\t// backed by entirely separate one-way curves. Deriving from the slug makes\n\t// the symbol a bijection with an already-unique, deposit-priced name.\n\t// The ledger reads THIS realm's height, so a fast-forwarded test chain\n\t// moves vote epochs with everything else. On an ordinary chain heightNow()\n\t// is exactly runtime.ChainHeight().\n\tcoin := grc20votes.NewLedgerWithClock(name, courtSymbol(slug), coinDecimals, epochBlocks, realmClock{})\n\tc := \u0026Court{\n\t\tid:             slug,\n\t\tname:           name,\n\t\ttier:           tierListed,\n\t\tadmin:          admin,\n\t\tcoin:           coin,\n\t\tgov:            governor.New(coin, coin),\n\t\tcrv:            curve.New(curveDenom, curveCap),\n\t\tescrow:         escrowAddr,\n\t\tclaims:         bptree.NewBPTree32(),\n\t\tqueue:          bptree.NewBPTree32(),\n\t\trecords:        bptree.NewBPTree32(),\n\t\tstandings:      bptree.NewBPTree32(),\n\t\tlocked:         bptree.NewBPTree32(),\n\t\tvoteLocks:      bptree.NewBPTree32(),\n\t\tstakeIdx:       bptree.NewBPTree32(),\n\t\tparams:         p.mustSane(),\n\t\tcreatedAt:      heightNow(),\n\t\tcurBudgetBpsFP: budgetBpsFPStart,\n\t}\n\tc.lastAccrual = c.createdAt\n\tif slug == metaSlug {\n\t\tc.absBudget = metaWeeklyBudget // §13.8: never S_live × bps\n\t}\n\tc.gov.Adopt(disputeKind{}, disputeRules(c.params))\n\tcourts.Set(slug, c)\n\tindexNewCourt(c) // directory ordering (burn-desc / creation-newest)\n\tif directoryAdmin == \"\" {\n\t\tdirectoryAdmin = admin\n\t}\n\treturn slug\n}\n\n// mustCourtName: 1..maxCourtNameLen. Capped so a purge-worthy name is bounded\n// and render stays predictable (MODERATION.md §2 user-text inventory).\n// maxCourtDescLen matches the overlay's own curation schema, which has validated\n// exactly this for as long as the description has been a browser-side field: 240\n// characters, and no newline. Single paragraph is not fussiness — the description\n// renders inline beside a court name in list rows, where a newline forges a row.\nconst maxCourtDescLen = 240\n\n// SetCourtDesc writes the court's description. One active moderator, the same\n// authority every other curation write takes and for the same reason recorded in\n// folders.gno: zero economic weight, instantly reversible, so the m-of-n ceremony\n// would tax routine curation for no security gain.\nfunc SetCourtDesc(cur realm, courtSlug, desc string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\trequireFolderMod(c, who)\n\tmustCourtDesc(desc)\n\tc.desc = desc\n\temitModAct(c.id, 0, \"court-desc\", who)\n}\n\n// SetCourtImage gives a court its face, on the same seat that writes its\n// description — a court's picture is curation, and curation is the moderators'.\n//\n// The argument is ONE line of the eight-field format a claim's evidence uses —\n// kind|sha256|mime|w|h|bytes|caption|mirrors — so web/media.js builds it with\n// the same code this realm parses with the same parser, and the CSP that already\n// allows the evidence hosts allows this with no second list.\n//\n// An image only, for the reason a folder's is: a video is a link the court\n// cannot vouch for, reasonable as EVIDENCE where the claim page says so, and\n// unreasonable as the face of a venue.\nfunc SetCourtImage(cur realm, courtSlug, media string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\trequireFolderMod(c, who)\n\titems := parseMediaArg(media)\n\tif len(items) != 1 {\n\t\tpanic(\"kourtv2: a court carries exactly one image\")\n\t}\n\tif items[0].kind != mediaKindImage {\n\t\tpanic(\"kourtv2: a court's image is an image, not a video link\")\n\t}\n\tc.image = \u0026items[0]\n\temitModAct(c.id, 0, \"court-image\", who)\n}\n\n// ClearCourtImage takes it off again. Separate from SetCourtImage because \"no\n// picture\" reached by passing an empty string is a state somebody arrives at by\n// accident.\nfunc ClearCourtImage(cur realm, courtSlug string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\trequireFolderMod(c, who)\n\tc.image = nil\n\temitModAct(c.id, 0, \"court-image-clear\", who)\n}\n\n// CourtImage is the one line back, or \"[]\" for a court with no picture — the\n// same empty shape FolderImage returns, so one client parser serves both.\nfunc CourtImage(courtSlug string) string {\n\tc := mustCourt(courtSlug)\n\tif c.image == nil {\n\t\treturn \"[]\"\n\t}\n\treturn encodeMedia([]mediaItem{*c.image})\n}\n\n// CourtImages answers many courts in one query:\n//\n//\t\"\u003cslug\u003e\\t\u003cone media line\u003e\\n…\"\n//\n// A court with no picture is SKIPPED rather than sent as an empty line, so a\n// directory of twenty courts where two have images is two records, not twenty.\n// slugs is comma-separated; unknown slugs are skipped, and at most\n// seriesPageMax are answered.\n//\n// WHY NOT ONE READ PER COURT. The directory draws every listed court, and a\n// per-court read is the fan-out DocketSeries exists to undo: each qeval reloads\n// this realm, so twenty courts would be twenty realm loads to fill twenty\n// thumbnails. Tab-separated because a media line is itself pipe-delimited and\n// carries a caption, and a caption may contain anything a person can type.\nfunc CourtImages(slugs string) string {\n\tout := strings.Builder{}\n\tsent := 0\n\tfor _, sl := range strings.Split(slugs, \",\") {\n\t\tif sent \u003e= seriesPageMax {\n\t\t\tbreak\n\t\t}\n\t\tsl = strings.TrimSpace(sl)\n\t\tif sl == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tv := courts.Get(sl)\n\t\tif v == nil {\n\t\t\tcontinue\n\t\t}\n\t\tc := v.(*Court)\n\t\tif c.image == nil {\n\t\t\tcontinue\n\t\t}\n\t\tout.WriteString(sl + \"\\t\" + encodeMedia([]mediaItem{*c.image}) + \"\\n\")\n\t\tsent++\n\t}\n\treturn out.String()\n}\n\nfunc mustCourtDesc(desc string) {\n\tif runeLen(desc) \u003e maxCourtDescLen {\n\t\tpanic(\"kourtv2: a court description is at most 240 characters\")\n\t}\n\tfor i := 0; i \u003c len(desc); i++ {\n\t\tif desc[i] == '\\n' || desc[i] == '\\r' {\n\t\t\tpanic(\"kourtv2: a court description is a single paragraph\")\n\t\t}\n\t}\n}\n\nfunc mustCourtName(name string) {\n\tif runeLen(name) == 0 || runeLen(name) \u003e maxCourtNameLen {\n\t\tpanic(\"kourtv2: a court name is 1..100 characters\")\n\t}\n}\n\n// ------------------------------------------------------------------ reads --\n\n// CourtDesc is the court's description, gated the way its name is. Empty for a\n// court that has none and for a purged one.\nfunc CourtDesc(courtSlug string) string {\n\treturn courtDescFor(mustCourt(courtSlug))\n}\n\n// CourtStakeOpenDelay is this court's polish window in blocks: how long after a\n// claim opens before staking may begin, and therefore how long its title stays\n// editable by its author (§9, EditClaimTitle).\n//\n// ZERO IS THE COMMON ANSWER and the reason this read exists. StartCourt passes\n// 0; only StartCourtP opts in. On a zero court the window has closed before the\n// opening transaction returns, so EditClaimTitle refuses every call — and a\n// client with no way to ask would have to either hide a live window or offer a\n// dead one. It could not know which, because nothing exposed the number.\n//\n// Ungated: a court's parameters are not secret, the same posture CourtName and\n// CourtTier take. No migration — the field is on every court already, zero by\n// default, so a court started before this read answers correctly without a write.\nfunc CourtStakeOpenDelay(courtSlug string) int64 {\n\treturn mustCourt(courtSlug).params.stakeOpenDelayBlocks\n}\n\n// CourtCount is how many courts have been started.\nfunc CourtCount() int { return courts.Size() }\n\n// Exists reports whether a slug names a court.\nfunc Exists(slug string) bool { return courts.Has(slug) }\n\nfunc CourtName(slug string) string { return mustCourt(slug).name }\n\n// CourtSymbol is a court coin's ticker — unique per court, derived from its\n// slug. Display it namespaced (KOURT:\u003cSYMBOL\u003e) anywhere outside its own court.\nfunc CourtSymbol(slug string) string { return mustCourt(slug).coin.Symbol() }\n\n// CoinDecimals is the scale every court coin uses. Exported because anything\n// quoting a court coin against another asset has to agree on it — a wrapper, a\n// pool, an indexer — and a hardcoded 6 elsewhere is a mispricing waiting for the\n// day this moves.\nfunc CoinDecimals() int { return coinDecimals }\nfunc CoinSupply(slug string) int64   { return mustCourt(slug).coin.TotalSupply() }\n\n// CoinPrice is the curve's marginal price for the next coin. There is no\n// \"backing\" read in V2: burned GNOT backs nothing, and rendering one would\n// imply a redemption value that does not exist (PLAN §3.8 pass 2).\nfunc CoinPrice(slug string) int64 { return mustCourt(slug).crv.Price(mustCourt(slug).minted) }\n\n// CourtEscrow is the address holding a court's staked CC, deposits, and bonds —\n// the realm's own address; each court's coin ledger isolates its balances.\nfunc CourtEscrow(slug string) address { return mustCourt(slug).escrow }\n\n// EmittedTotal is every CC this court has minted through emission — the figure\n// a holder audits dilution against.\nfunc EmittedTotal(slug string) int64 { return mustCourt(slug).emittedTotal }\n\n// effMinAnswerX / effMinDeposit are the LIVE floors: a fraction of the court's\n// OWN supply, with each court's frozen params serving as the dust arm.\n//\n// TWO LANES. v0.15 made this supply-relative for META. v0.27 extends it to every\n// court — the premise that ordinary courts were safe was wrong, since StartCourt\n// lets a creator set `stakeOpenDelayBlocks` and nothing else — but at a TENTH of\n// meta's fraction, because the two are not the same kind of price. See the\n// constant block: an ordinary court's claims are the product and their floors\n// SUM out of one custodial escrow, so the fraction decides how many claims the\n// court may run at once; meta's claim count is O(1) forever.\n//\n// The params are the dust arm rather than a separate constant, which keeps\n// `minAnswerX` alive and meaningful — a creator-chosen anti-rounding floor,\n// exactly as `minClaimDepositCC` already serves effMinDeposit — instead of\n// leaving it read only by mustSane.\nfunc effMinAnswerX(c *Court) int64 {\n\tif c.id == metaSlug {\n\t\treturn supplyFloor(c, answerXFloorBps, c.params.minAnswerX)\n\t}\n\treturn supplyFloor(c, ordAnswerXFloorBps, c.params.minAnswerX)\n}\n\nfunc effMinDeposit(c *Court) int64 {\n\t// Unpegged from effMinAnswerX/100 (which failed §5.3 arithmetically: a\n\t// 50-row flood parked half of ONE self-answer escape). Its own fraction now,\n\t// sized by the deploy invariant `50·depositFloorBps ≥ 1.5·answerXFloorBps`.\n\tif c.id == metaSlug {\n\t\treturn supplyFloor(c, depositFloorBps, c.params.minClaimDepositCC)\n\t}\n\treturn supplyFloor(c, ordDepositFloorBps, c.params.minClaimDepositCC)\n}\n\n// supplyFloor is max(dust, bps·supply), clamped so the resulting filing cost\n// can never exceed the turnout bar. Three deliberate choices:\n//\n//   - RAW supply, not votable. Votable is deflatable for free — anyone parking\n//     no-loss stake in the escrow shrinks it — so a votable base would let an\n//     attacker lower the appeals bar at zero cost, and would make each new\n//     appeal cheapen the next.\n//   - The LAST SEALED epoch, not TotalSupply(). Live supply is atomically\n//     griefable: PostAnswer reads this floor (answer.gno), ClaimMetaFranchise\n//     mints into supply, so a whale sitting on unclaimed franchise could\n//     front-run an honest answer in the same block, push the floor above the\n//     appellant's X̄, and revert it — repeatably, for free, keeping coins they\n//     were owed anyway. A sealed read closes that for nothing.\n//   - CLAMPED, the v0.14 lesson applied to this lane. The deploy invariant\n//     covers the ratio arm, but no constant can see the dust arm: below ~30 CC\n//     of supply the 1 CC dust floor prices filing above the 5% turnout bar and\n//     inverts the guarantee, exactly as carrotClampMinCC did to the election bond.\n// maxAnswerBondBps is the largest bond PostAnswer can demand, in bps of X̄ — the\n// draw arm at the hot-rate 12-week maximum, or the base arm if that is larger.\n//\n// It exists because \"the bond\" stopped meaning answerBondBps. PostAnswer's floor\n// raises the bond toward 1.6× the destroyable draw, so anything pricing a cost\n// against the bond must use the worst case or it under-prices by up to 5×.\n// mustInvariants pins this against its own copy of the arithmetic so the two\n// cannot drift.\nfunc maxAnswerBondBps() int64 {\n\tmaxRateBpsFP := 255 * (r0WeeklyBps*1_000_000 + budgetBpsFPStart) / 100\n\tmaxMidGrossBps := maxRateBpsFP * (deadClaimTimeout / periodBlocks) / 1_000_000\n\tif arm := bondFloorConvBps * maxMidGrossBps / 10000; arm \u003e answerBondBps {\n\t\treturn arm\n\t}\n\treturn answerBondBps\n}\n\nfunc supplyFloor(c *Court, bps, dust int64) int64 {\n\t// Epoch() is 1-based and epoch 0 means \"before this token existed\", which\n\t// PastTotal refuses rather than reporting as zero. Unlike quorumFloor, this\n\t// runs at OpenClaim — reachable in meta's very first epoch — so the\n\t// pre-history case is real and resolves to the dust arm.\n\tvar supply int64\n\tif at := c.coin.Epoch(); at \u003e 1 {\n\t\tsupply = c.coin.PastTotal(at - 1)\n\t}\n\tv := mulDiv128(supply, bps, grc20votes.Bps)\n\tif v \u003c dust {\n\t\tv = dust\n\t}\n\t// Filing costs (Bps+bond)/Bps × X̄; hold that STRICTLY under the 5% bar. The\n\t// obvious lid — 5%·supply × Bps/(Bps+bond) — makes filing exactly EQUAL the\n\t// bar at every supply, so the runtime clamp admitted the very equality\n\t// mustInvariants rejects on the constants. One bps of slack restores the\n\t// strict inequality, and it matters precisely where the lid binds: the\n\t// low-supply case the dust arm reaches.\n\t//\n\t// PRICED OFF THE WORST-CASE BOND, not the base. PostAnswer's floor can raise\n\t// the bond to the draw arm's 3083 bps, so pricing this off answerBondBps alone\n\t// admitted filing at 6.17% of supply against the 5% bar — a 23.4% overshoot of\n\t// this exact invariant, on a small sealed court with an aged claim. Harmless\n\t// at 5000 bps, where the floor never exceeded the base; live at 600.\n\tif lid := mulDiv128(supply, quorumSupplyBps, grc20votes.Bps) *\n\t\t(grc20votes.Bps - 1) / (grc20votes.Bps + maxAnswerBondBps()); lid \u003e 0 \u0026\u0026 v \u003e lid {\n\t\tv = lid\n\t}\n\tif v \u003c 1 {\n\t\tv = 1\n\t}\n\treturn v\n}\n\n// mustCourt fetches a court or panics. Never exported.\nfunc mustCourt(slug string) *Court {\n\tv := courts.Get(slug)\n\tif v == nil {\n\t\tpanic(\"kourtv2: no such court\")\n\t}\n\treturn v.(*Court)\n}\n\n// courtSymbol is the per-court coin symbol: the slug upper-cased, hyphens kept.\n// Keeping hyphens matters — stripping them would break injectivity (\"ab-c\" and\n// \"abc\" would collide) and silently reintroduce the shared-symbol problem.\n// Render namespaces it as KOURT:\u003cSYMBOL\u003e; a bare symbol appears only inside its\n// own court, so a court coin can never read as the platform token.\nfunc courtSymbol(slug string) string { return strings.ToUpper(slug) }\n\n// reservedSlugs may never be registered: each would let a court impersonate the\n// platform token or a well-known asset in any wallet that shows only symbols.\n// (metaSlug is deliberately NOT listed: init must be able to create it, and the\n// PUBLIC path refuses it separately in startCourtUser.)\n// \"court\" is reserved alongside \"kourt\": it is a homophone of the platform\n// ticker, and a court coin symbol is the upper-cased slug, so leaving it open\n// would let someone mint COURT and have wallets show it beside KOURT.\nvar reservedSlugs = []string{\n\t\"kourt\", \"court\", \"gno\", \"gnot\", \"ugnot\",\n\t\"btc\", \"eth\", \"usdc\", \"usdt\",\n}\n\n// mustSlug: 1..maxSlugLen of [a-z0-9] — URL- and markdown-safe by construction,\n// and short enough that courtSymbol always satisfies GRC20's symbol-length bound.\n// The hyphen was dropped: the coin symbol is the upper-cased slug, and KOURT:MY-COURT\n// reads a hyphen as a minus. Narrowing is the safe direction here — see\n// TestSlugCharsetIsWhatMakesTheRawRenderSafe, which exists because render.gno writes\n// the slug RAW into markdown link targets.\nfunc mustSlug(slug string) {\n\tif len(slug) == 0 || len(slug) \u003e maxSlugLen {\n\t\tpanic(\"kourtv2: a slug is 1..11 characters\")\n\t}\n\tfor _, r := range reservedSlugs {\n\t\tif slug == r {\n\t\t\tpanic(\"kourtv2: that slug is reserved\")\n\t\t}\n\t}\n\tfor i := 0; i \u003c len(slug); i++ {\n\t\tch := slug[i]\n\t\tok := (ch \u003e= 'a' \u0026\u0026 ch \u003c= 'z') || (ch \u003e= '0' \u0026\u0026 ch \u003c= '9')\n\t\tif !ok {\n\t\t\tpanic(\"kourtv2: a slug is lowercase letters and digits only\")\n\t\t}\n\t}\n}\n"},{"name":"courtburn.gno","body":"// The court-creation burn: a minimum GNOT payment, burned, to open a court.\n//\n// THIS REVERSES A RECORDED DECISION, so it says so rather than quietly\n// contradicting it. MODERATION.md §13.5 logged \"StartCourt creation fee —\n// RESOLVED (v0.8.2): no GNOT fee\", and the reasoning was sound: a fee written\n// into the source as a constant cannot be sized, because GNOT has no on-chain\n// USD oracle, so whatever number is chosen is wrong at some future price and\n// nobody can tell when. Court-count floods were left to the per-byte storage\n// deposit, which is protocol-set and reprices itself.\n//\n// WHAT CHANGES THE ANSWER is that this fee is not a constant. It is set by the\n// global DAO admin, at whatever the price is on the day, and reset when that\n// price moves — which is exactly the oracle the objection said was missing, in\n// the form the objection did not consider: a human. The storage deposit still\n// does its job; this sits on top for the case the deposit is too cheap to deter\n// somebody who wants a hundred slugs.\n//\n// DEFAULT ZERO, WHICH IS OFF, and that is deliberate rather than unfinished.\n// Zero preserves everything v0.8.2 was protecting: no unsizable number is\n// baked into the source, and StartCourt stays realm-callable, because a realm\n// cannot make a user call and a burn that demanded one would lock every realm\n// out of court creation. Set it and the price applies from the next call; set\n// it back to zero and the path is exactly what it was. The seed sets a non-zero\n// value so the mechanism is visible on a live chain rather than only in tests.\n//\n// NOT COUNTED AS THE COURT'S BURN. reindexBurn/accrueFranchise are for GNOT a\n// buyer chose to sink into a court's curve — the front page ranks on that\n// figure precisely because it is voluntary and cannot be faked. A mandatory\n// toll every court pays identically adds a constant to every row: it moves no\n// ranking, means nothing about a court, and would make the directory's one\n// un-sybil-able signal slightly less honest. So it goes to the sink and is\n// counted only in the realm-wide BurnedGNOT total.\npackage kourtv2\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime/unsafe\"\n)\n\n// courtCreationBurnInit is the shipped default: zero, meaning no burn is\n// required and StartCourt behaves exactly as it did before this file existed.\nconst courtCreationBurnInit = int64(0)\n\nvar courtCreationBurn = courtCreationBurnInit\n\n// CourtCreationBurn is the µGNOT a new court currently costs to open. Zero\n// means the burn is off. Published so a page can quote the price BEFORE the\n// user signs a transaction that would otherwise panic on them.\nfunc CourtCreationBurn() int64 { return courtCreationBurn }\n\n// SetCourtCreationBurn prices court creation, in µGNOT. 0 turns the burn off.\n//\n// Gated on the global DAO admin, matching SetAssociationBondDefault — the two\n// are the same kind of knob (a realm-wide default that prices a stranger's\n// entry) and splitting their authority would be a distinction with no meaning.\n// Not moderator-settable: a court's moderators govern THEIR court, and there is\n// no court yet when this price is charged.\nfunc SetCourtCreationBurn(cur realm, amount int64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\td := ensureGlobalDAO()\n\tif cur.Previous().Address() != d.admin {\n\t\tpanic(\"kourtv2: only the global DAO admin prices court creation\")\n\t}\n\tmustSanePrice(amount)\n\tcourtCreationBurn = amount\n}\n\n// ugnotAmt writes µGNOT as a trimmed decimal, for a refusal a human can read.\n//\n// It defers to ccAmt because GNOT and CC carry the SAME six decimals, so the\n// arithmetic is identical and a second copy of it would be a second thing to\n// get wrong. The wrapper exists rather than a bare ccAmt call at the two sites\n// so that the denomination is stated where the string is built: a message\n// reading \"burns 2.5\" is a different promise depending on which coin it means.\nfunc ugnotAmt(n int64) string { return ccAmt(n) }\n\n// mustSanePrice refuses a price that cannot mean anything. Its own function\n// rather than two lines inside the setter, because the harness cannot reach it\n// otherwise: testing.SetRealm's caller override does not survive into the\n// closure uassert.AbortsContains runs, so an ADMIN call that is supposed to\n// abort cannot be expressed as an abort assertion — measured, not assumed, and\n// the arm failed with \"only the global DAO admin prices court creation\" while\n// holding the admin's own address. A pure value check needs no caller and no\n// crossing, so lifting it out makes it directly testable while leaving the\n// production order intact: authorize first, then validate.\nfunc mustSanePrice(amount int64) {\n\tif amount \u003c 0 {\n\t\tpanic(\"kourtv2: a court-creation burn cannot be negative\")\n\t}\n}\n\n// takeCourtCreationBurn collects the creation payment and burns ALL of it, so\n// the configured figure is a floor and nothing is ever paid back out. It is\n// called by the PUBLIC creation entrypoints only — realm init\n// builds the meta court through startCourt directly, before a DAO, an admin or\n// a bank balance exists, and charging it there would make the realm undeployable.\n//\n// THE GUARD IS IsUserCall, NOT IsUser, and the difference is the whole point:\n// IsUser also admits a `maketx run` ephemeral realm, which can consume the\n// origin-send envelope and call in with it already spent — so the check would\n// pass while the coins were gone. Reading unsafe.OriginSend under\n// IsCurrent + IsUserCall is the sanctioned inbound-payment shape, and it is the\n// same one Buy uses twenty lines into buy.gno.\n//\n// `cur` is the crossing entrypoint's own realm value, so IsCurrent is checked\n// here rather than assumed: this is a secondary realm parameter by the time it\n// reaches this function, and a stale one would name the wrong caller.\nfunc takeCourtCreationBurn(cur realm) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tprice := courtCreationBurn\n\tif price \u003c= 0 {\n\t\t// OFF. Nothing is read, nothing is required, and in particular no\n\t\t// IsUserCall check happens — that is what keeps StartCourt callable by\n\t\t// another realm while the burn is unpriced.\n\t\treturn\n\t}\n\tprev := cur.Previous()\n\tif !prev.IsUserCall() {\n\t\tpanic(\"kourtv2: starting a court burns GNOT, so it must be a direct user call\")\n\t}\n\tsent := unsafe.OriginSend().AmountOf(gnotDenom)\n\tif sent \u003c price {\n\t\t// Named amounts, both of them: a refusal that does not say what was\n\t\t// wanted and what arrived leaves the caller to guess at the difference.\n\t\tpanic(\"kourtv2: starting a court burns at least \" + ugnotAmt(price) +\n\t\t\t\" GNOT, you sent \" + ugnotAmt(sent))\n\t}\n\t// THE WHOLE PAYMENT BURNS, and that is what makes this a MINIMUM rather\n\t// than a price with change.\n\t//\n\t// The first cut burned `price` and refunded the excess, copying Buy. That\n\t// version was wrong here, and check-nontransferable caught it: a refund is\n\t// GNOT LEAVING THE REALM TO A USER, which is the one thing this realm's\n\t// regulatory position rests on never happening (\"the payment is burned,\n\t// nothing ever redeems\" — REGULATIONS.md). Buy's remainder send is the sole\n\t// sanctioned exception and it exists because a bonding curve genuinely\n\t// cannot spend the last ugnot; there is no such arithmetic here.\n\t//\n\t// Burning all of it removes the refund instead of arguing for it, and the\n\t// semantics come out cleaner: send at least the minimum and all of it goes\n\t// to the keyless sink. Nobody is overcharged by surprise, because\n\t// CourtCreationBurn() publishes the figure and the page attaches exactly\n\t// it; a caller who deliberately sends more has deliberately burned more.\n\t// Aborting above reverts the incoming send, so a caller who underpays keeps\n\t// every coin; only from here does anything move.\n\tb := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tb.SendCoins(cur.Address(), chain.PackageAddress(burnSinkPath),\n\t\tchain.Coins{chain.NewCoin(gnotDenom, sent)})\n}\n"},{"name":"directory.gno","body":"package kourtv2\n\nimport \"strconv\"\n\n// The directory: curation-only tiers, exactly V1's model — a hidden court still\n// works for anyone holding its address; moderation, not custody.\n\nconst (\n\ttierHidden   uint8 = 0\n\ttierListed   uint8 = 1\n\ttierFeatured uint8 = 2\n)\n\n// directoryAdmin is the first court's creator. It still bootstraps the global\n// DAO (ensureGlobalDAO makes it the sole founding member), but it is no longer\n// an authority in its own right — see SetTier.\nvar directoryAdmin address\n\n// SetTier moves a court between the three directory tiers: 0 hidden, 1 listed,\n// 2 featured. Hidden courts are kept off the front page; featured ones are\n// promoted on it.\n//\n// Any member of the global DAO, single key — a tier change moves no money,\n// removes no text, and is undone by one call in the other direction. Setting a\n// court to the tier it already holds writes nothing and records nothing.\n//\n// Emits a court-level event carrying the tier CODE only, never any court text,\n// because events cannot be purged. There is no moderation-log row: the log is\n// per-claim and this is court-level.\n//\n// Refused when the court does not exist, the caller is not a global DAO member,\n// or `tier` is outside 0..2.\nfunc SetTier(cur realm, courtSlug string, tier int) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tif !isGlobalMod(who) {\n\t\tpanic(\"kourtv2: only a global DAO member may set a court's tier\")\n\t}\n\tif tier \u003c int(tierHidden) || tier \u003e int(tierFeatured) {\n\t\tpanic(\"kourtv2: tier is 0 (hidden), 1 (listed), or 2 (featured)\")\n\t}\n\tc := mustCourt(courtSlug)\n\tif c.tier == uint8(tier) {\n\t\treturn // a no-op writes nothing and stamps no history\n\t}\n\tc.tier = uint8(tier)\n\temitGlobalAct(c.id, 0, \"tier:\"+strconv.Itoa(tier), who)\n}\n\n// DirectoryAdmin is who curates tiers; CourtTier is a court's tier.\nfunc DirectoryAdmin() address        { return directoryAdmin }\nfunc CourtTier(courtSlug string) int { return int(mustCourt(courtSlug).tier) }\n\n// ListCourts returns every court's slug in slug order (lexicographic — the key\n// the tree holds them under, not age).\nfunc ListCourts() []string {\n\tout := []string{}\n\tcourts.Iterate(\"\", \"\", func(key string, _ any) bool {\n\t\tout = append(out, key)\n\t\treturn false\n\t})\n\treturn out\n}\n\n// ListByTier returns the slugs at exactly one tier.\nfunc ListByTier(tier int) []string {\n\tout := []string{}\n\tcourts.Iterate(\"\", \"\", func(key string, v any) bool {\n\t\tif int(v.(*Court).tier) == tier {\n\t\t\tout = append(out, key)\n\t\t}\n\t\treturn false\n\t})\n\treturn out\n}\n"},{"name":"dispute.gno","body":"// The dispute machine — V1's audited round structure (doubling bonds, sealed\n// 7-day tallies, tally-based classification, escrow windows, reopens, 3-round\n// close) with the V2 dispositions (PLAN §3.6, v0.11–v0.25):\n//\n//\tFORFEITURES BURN; COMPENSATION MINTS. No value ever moves between\n//\tadversaries on any outcome — punishment is deflation, reward is issuance,\n//\tsenior-queued: comp = min(2 × the prevailing party's own bond,\n//\tfloor(80% × the loser's burned bond)), reserved at the round's decision.\n//\n//\tFailed (quorum-less) rounds burn HALF the disputer's bond and return half\n//\t(turnout is not the disputer's conduct); the answerer is NOT comped on a\n//\tfailed round — their bond simply survives, escrowed until the verdict is\n//\tfinal. Three failed rounds close the claim without a decision: everyone\n//\t1×, deposit AND fee refund, no draw.\n//\n//\tEvery %·X̄ quantity reads the ONE X̄frozen snapshot taken at the answer\n//\t(P6): bond base = min(20%·X̄frozen, 40% × the actual answer bond),\n//\tdoubling per failed round, claim-scoped (sybil-proof).\n//\n// Rounds are framed against the ORIGINAL answer (V1): overturn sets the\n// provisional verdict to its opposite, uphold to the answer itself; a reopen\n// vote can flip it back. Bonds are disposed at each round's decision (the\n// per-event burn must accompany its comp reservation — A19's invariant); the\n// answer bond is burned only by the round that decides against it and returned\n// only at VERDICT_FINAL. Answer records move ONCE, at Finalize, from the final\n// verdict (P13) — intermediate rounds never churn the record.\n\npackage kourtv2\n\nimport (\n\t\"strconv\"\n\n\tgovernor \"gno.land/p/kourt/governor/v0\"\n\tgrc20votes \"gno.land/p/kourt/grc20votes/v0\"\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\nconst disputeKindName = \"dispute\"\n\n// quorumSupplyBps is the absolute turnout floor's supply arm: 5%.\nconst quorumSupplyBps = int64(500)\n\n// finalizeGraceBlocks is the week Finalize stays participant-only after the\n// escrow window lapses (v0.11 A13: a stranger must not pick the settle block).\nconst finalizeGraceBlocks = int64(120_960)\n\ntype disputeKind struct{}\n\nfunc (disputeKind) Name() string { return disputeKindName }\nfunc (disputeKind) Describe(payload string) string {\n\treturn \"resolve a disputed claim by a token vote (see the court page for the claim)\"\n}\nfunc (disputeKind) Check(payload string) error { return nil } // the court validates\n\n// Do is unused: the court resolves by reading the tally, never by\n// governor.Execute. It exists only to satisfy the Kind seam.\nfunc (disputeKind) Do(_ int, rlm realm, payload string) error { return nil }\n\n// disputeRules mirror V1: ThresholdBps is the overturn bar; the escrow window,\n// not the governor timelock, is the reaction period.\nfunc disputeRules(p Params) governor.Rules {\n\treturn governor.NewRules(5000, p.disputeThresholdBps, p.votingBlocks, 1, p.graceBlocks, 0)\n}\n\n// OpenDispute contests a claim's answer. The caller posts the round's bond and\n// the court opens a sealed governor vote (\"OVERTURN the answer?\") with an\n// absolute quorum floor. The first round must open inside the 72h settle\n// delay; reopens must open inside the escrow window of a standing provisional\n// verdict. Returns the governor proposal id.\nfunc OpenDispute(cur realm, courtSlug string, claimID uint64) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif cs.frozenAt == 0 {\n\t\tpanic(\"kourtv2: there is no answer to dispute\")\n\t}\n\tif who == cs.answerer {\n\t\t// Hygiene only (a sybil wallet trivially evades it): the REAL\n\t\t// credential defense is the credEligible weight bar below (M2-1).\n\t\tpanic(\"kourtv2: the answerer cannot dispute their own answer\")\n\t}\n\tif cs.disputeOpen {\n\t\tpanic(\"kourtv2: a dispute is already open on this claim\")\n\t}\n\tif cs.verdictAt != 0 {\n\t\tpanic(\"kourtv2: the verdict is final\")\n\t}\n\tnow := heightNow()\n\tif cs.provisional \u003c 0 {\n\t\t// First round: the 72h settle delay IS the dispute window — after it,\n\t\t// only SettleUndisputed. The two guards partition every height, so no\n\t\t// settle/dispute race exists.\n\t\tif passed, known := pastDeadline(cs.answeredAtTime, settleSecs); (known \u0026\u0026 passed) ||\n\t\t\t(!known \u0026\u0026 now \u003e= cs.answerHeight+settleDelay) {\n\t\t\tpanic(\"kourtv2: the dispute window has passed; the answer settles undisputed\")\n\t\t}\n\t} else if escrowPassed(cs) {\n\t\tpanic(\"kourtv2: the escrow window has passed; Finalize the verdict\")\n\t}\n\ttouch(c)\n\n\t// Bond: min(20%·X̄frozen, 40% × the actual answer bond) — the v0.25 arm\n\t// keeps round 1 at q \u003e 1/3 on every claim including bond-capped courts —\n\t// doubled per failed round, claim-scoped (a fresh disputer address cannot\n\t// reset the multiplier). provClose bounds failedRounds ≤ 2 here; the\n\t// clamp guards the shift regardless.\n\tbond := cs.disputeBond0()\n\tmustSpendable(c, who, bond)\n\tc.coin.Transfer(who, c.escrow, bond)\n\n\tfloor := quorumFloor(c, cs.xBarFrozen)\n\tcs.round++\n\tpayload := strconv.FormatUint(claimID, 10) + \"|\" + strconv.FormatInt(cs.round, 10)\n\tpid := c.gov.ProposeWithQuorum(c.escrow, disputeKindName, payload, \"dispute\", floor)\n\n\tcs.proposalID = pid\n\tcs.disputeOpen = true\n\tcs.disputeOpenedTime = nowTime()\n\tcs.disputeBond = bond\n\tcs.disputer = who\n\treturn pid\n}\n\n// VoteDispute casts a vote on a claim's open dispute — \"yes\" to overturn the\n// answer, \"no\" to uphold it, \"spam\" to say the claim was not worth judging.\n//\n// SPAM REPLACED ABSTAIN, and it is the same bucket underneath. Abstain was\n// strictly dominated: it locked the same coin and added the same turnout as a\n// side vote, while the carrot pays only voters who matched the verdict, so it\n// had every cost and no upside and no payoff-maximiser ever pressed it. The\n// bucket's MECHANICS are exactly what a spam flag wants, though — the governor\n// counts it toward quorum (turnout = yes+no+abstain) and excludes it from the\n// verdict bar (forAndAgainst = yes+no) — so the slot is repurposed rather than\n// a fourth one added.\n//\n// What it now DOES is elsewhere: past half the weight cast, the claim is\n// discarded; below that it discounts the claim's reward in proportion.\nfunc VoteDispute(cur realm, courtSlug string, claimID uint64, choice string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif !cs.disputeOpen {\n\t\tpanic(\"kourtv2: no open dispute on this claim\")\n\t}\n\t// Nobody votes on their own claim's verdict — the same rule VoteQuality has\n\t// applied since V2, for the same reason: a participant's stake, bond or\n\t// authorship is a direct interest in the outcome, and quality already refuses\n\t// to let anyone vote their own payout multiplier.\n\t//\n\t// THIS IS HYGIENE, NOT A SECURITY GUARD, and the distinction matters because\n\t// it would be easy to bank on. isParticipant is ADDRESS-KEYED, and this\n\t// design's root principle — earned over four audit rounds and stated at the\n\t// top of MODERATION.md — is that address-keyed defenses fall to sybils; only\n\t// capital-keyed, deadline-keyed or residency-keyed ones hold. An attacker\n\t// wanting to uphold their own answer simply votes from an address that never\n\t// staked, which costs nothing. So this closes the naive case and the\n\t// conflict of interest; it does NOT close the manufactured contest.\n\t//\n\t// What closes that is capital-keyed and already shipped: credEligible\n\t// requires weight \u003e= floor/4 voted AGAINST the claim before an aggressive\n\t// meta verb will execute, so a contest with no real adversary buys nothing\n\t// however many addresses staged it.\n\tif isParticipant(cs, who) {\n\t\tpanic(\"kourtv2: a participant may not vote on their own claim's verdict\")\n\t}\n\t// The cap is what the voter still holds. The governor keeps deriving the\n\t// snapshot itself and takes the lesser, so this can only ever LOWER — which is\n\t// why the dispute lane goes through VoteWithCap instead of clamping here. The\n\t// tally is the governor's, and `rest := p.total - cast` is its invariant to\n\t// keep; an earlier attempt handed it a finished weight instead of a ceiling and\n\t// that let `yes` exceed anything the electorate held while the early arms'\n\t// denominator stayed snapshotted at (total - abstain) — a proposal passing on\n\t// support that did not exist. (An earlier comment here said it \"dropped `no`\n\t// out of the early-decide test\"; that reduction holds identically either way,\n\t// so it was never the symptom. governor.gno has the algebra.)\n\t//\n\t// Refused here rather than in the governor so the message is this court's. See\n\t// quality.gno for the derivation, and VOTEFLOOR.md for why the snapshot alone\n\t// was rentable: this is the lane the end-to-end rental was demonstrated on.\n\theld := voteCap(c, who)\n\tif held \u003c= 0 {\n\t\t// \"NO LONGER\" WOULD BE A GUESS HERE, and for most people a wrong one.\n\t\t// voteCap is the balance NOW, and this guard has no snapshot check in\n\t\t// front of it — unlike quality.gno and modvote.gno, which refuse on\n\t\t// `snapshot \u003c= 0` first and so have earned that word by the time they\n\t\t// reach it. Here a holder who sold and a stranger who never held a unit\n\t\t// arrive at the same line, and the second is the common case: it is the\n\t\t// message anyone who finds the court and tries to vote will read.\n\t\tpanic(\"kourtv2: you hold none of this court's coin, and voting weight is \" +\n\t\t\t\"the lesser of what you held when the round opened and what you hold now\")\n\t}\n\t// TWO VOCABULARIES, ONE TRANSLATION POINT. The court says \"spam\"; the\n\t// governor says \"abstain\", which is its own word for the bucket that counts\n\t// toward quorum and not toward a side. The p/ layer's vocabulary is not ours\n\t// to rename, so the mapping happens here and nowhere else.\n\t//\n\t// The literal \"abstain\" is REFUSED rather than accepted as a synonym. Two\n\t// names for one bucket is how a client and a realm come to disagree about\n\t// what was cast — and a caller still sending \"abstain\" is a caller written\n\t// against the old meaning, which no longer describes what the vote does.\n\tgovChoice := choice\n\tswitch choice {\n\tcase voteYes, voteNo:\n\tcase voteSpam:\n\t\tgovChoice = govAbstain\n\tdefault:\n\t\tpanic(\"kourtv2: a dispute vote is \\\"yes\\\" to overturn, \\\"no\\\" to uphold, \" +\n\t\t\t\"or \\\"spam\\\" to say the claim was not worth judging\")\n\t}\n\tc.gov.VoteWithCap(who, cs.proposalID, govChoice, held)\n\t// Court-side record of the choice AND THE WEIGHT: the governor's roll is\n\t// reclaimable after resolution (ReleaseRoll is permissionless), so the carrot\n\t// must not depend on it.\n\t//\n\t// The weight used to be recomputed later from PastVotes at the proposal's\n\t// epoch, which was exactly equal and needed no storage. It is stored because\n\t// equal-by-coincidence is a bad thing for a payout to rest on — and THAT\n\t// COINCIDENCE IS NOW GONE: the line above caps the tally at the voter's live\n\t// balance, so a recomputed PastVotes numerator would exceed what actually\n\t// counted toward the denominator and the carrot would over-pay every voter who\n\t// was capped. What was defensive when written is load-bearing now. Read back\n\t// from the governor here, where the roll is certainly intact because this line\n\t// just voted.\n\tif cs.voted == nil {\n\t\tcs.voted = bptree.NewBPTree32()\n\t}\n\t// THE COURT'S WORD, not the governor's: this record is what PullCarrot\n\t// compares against cs.carrotChoice, and that is set in this realm's\n\t// vocabulary. Storing govChoice here would make a spam voter's record read\n\t// \"abstain\" and never match the \"spam\" the discard path records.\n\tcs.voted.Set(\"d\"+beClaimKey(uint64(cs.proposalID))+string(who), choice)\n\t_, dw, ok := c.gov.VoteOf(cs.proposalID, who)\n\tif !ok {\n\t\tpanic(\"kourtv2: the governor did not record the vote it just accepted\")\n\t}\n\tcs.voted.Set(\"dw\"+beClaimKey(uint64(cs.proposalID))+string(who), dw)\n\t// AND THE COIN IS COMMITTED UNTIL THIS ROUND RESOLVES. The floor above stops a\n\t// renter voting weight they have already handed back; this stops them handing\n\t// it back the moment after. Measured on the floor alone: the rental survived as\n\t// vote-then-sell at 599x the quorum floor. Locked at the weight the governor\n\t// tallied, not at the balance — a voter capped down should be committed to what\n\t// their ballot was worth and no more. See votelock.gno.\n\tlockVote(c, who, voteLockDispute, int64(claimID), cs.proposalID, dw)\n}\n\n// ResolveDispute reads the FROZEN tally of a closed dispute vote and\n// classifies it — failed-quorum, overturn, or uphold — disposing this round's\n// bonds under the burn/mint doctrine and recording the provisional verdict.\n// Permissionless; only once the vote has closed. The verdict is derived from\n// the tally, never the drifting State label (V1's classifier).\nfunc ResolveDispute(cur realm, courtSlug string, claimID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif !cs.disputeOpen {\n\t\tpanic(\"kourtv2: no dispute to resolve\")\n\t}\n\tif c.gov.State(cs.proposalID) == \"active\" {\n\t\tpanic(\"kourtv2: the dispute vote is still open\")\n\t}\n\ttouch(c)\n\n\t// spamW, not \"abstain\": the governor's third bucket is the court's spam flag\n\t// (VoteDispute translates at the boundary), and calling it by the p/ layer's\n\t// name here is how the two meanings drift apart again.\n\tyes, no, spamW, _ := c.gov.Tally(cs.proposalID)\n\tcast := yes + no + spamW\n\tfloor := c.gov.QuorumFloor(cs.proposalID)\n\tfirstResolution := cs.provisional \u003c 0 // capture BEFORE the switch mutates it\n\n\tswitch {\n\tcase cast \u003c floor:\n\t\t// FAILED-QUORUM: apathy must not resolve a claim — and it is not the\n\t\t// disputer's conduct (v0.25): HALF the bond returns, half burns\n\t\t// (floor favors the burn; dust deflates). No comp mints — the\n\t\t// answerer's bond simply survives, still escrowed.\n\t\thalf := cs.disputeBond / 2\n\t\tif half \u003e 0 {\n\t\t\tc.coin.Transfer(c.escrow, cs.disputer, half)\n\t\t}\n\t\tif rest := cs.disputeBond - half; rest \u003e 0 {\n\t\t\tc.coin.Burn(c.escrow, rest)\n\t\t}\n\t\tcs.disputeBond = 0\n\t\t// The VERDICT question failed quorum, but the RESERVE's question has its own,\n\t\t// higher bar and can be answered on a round the governor's floor rejected\n\t\t// (v0.51). Scoring the ride here rather than re-arming blindly is what stops\n\t\t// a full-bar rescue tally from being discarded and then burned by the\n\t\t// unconditional ResolveSlashWindow — a below-bar ride still re-arms, which is\n\t\t// the v0.47 behavior verbatim.\n\t\t//\n\t\t// Only the FIRST failed round defaults the verdict to the answer; a\n\t\t// failed reopen leaves a standing decided verdict untouched.\n\t\tif firstResolution {\n\t\t\tcs.provisional = cs.answer\n\t\t}\n\t\tcs.failedRounds++\n\t\tif cs.failedRounds \u003e= maxFailedRounds {\n\t\t\tprovCloseClaim(c, cs)\n\t\t}\n\t// DISCARDED, and its position in this switch is the protection. It sits\n\t// AFTER the failed-quorum arm, so a discard is a fraction of a turnout that\n\t// already cleared min(X̄, votable/3) — three voters cannot delete a claim.\n\t// And BEFORE the verdict arms, because a claim the court threw out has no\n\t// verdict to reach: whether the answer was right is not a question worth\n\t// settling once the electorate has said the claim should not have been asked.\n\tcase spamDiscards(spamW, cast):\n\t\tspamDiscardClaim(c, cs, spamW, cast)\n\tcase yes \u003e 0 \u0026\u0026 yes*grc20votes.Bps \u003e= (yes+no)*c.params.disputeThresholdBps:\n\t\t// OVERTURN: the answer was wrong. The answer bond BURNS in full (the\n\t\t// forfeiture); the disputer's own bond returns; comp mints\n\t\t// senior-queued, capped by both arms (v0.20). A reopen after a prior\n\t\t// overturn finds answerBond already zero — comp's 80% arm is then 0\n\t\t// and nothing mints (no burn happened this round to anchor it).\n\t\tposted := cs.disputeBond // the disputer's own risk, captured pre-zeroing\n\t\tburned := cs.answerBond\n\t\tif burned \u003e 0 {\n\t\t\tc.coin.Burn(c.escrow, burned)\n\t\t\tcs.answerBond = 0\n\t\t\t// STAMP THE WINNER HERE, INSIDE THE FORFEITURE, and not below with\n\t\t\t// the rest of the round bookkeeping. The credit is for causing a\n\t\t\t// forfeiture, so it belongs to the round that caused one — and only\n\t\t\t// the first overturn ever does: it zeroes answerBond, so a second\n\t\t\t// overturn burns nothing, returns its own bond whole, mints no comp,\n\t\t\t// and would otherwise re-stamp this field at zero net cost and take\n\t\t\t// the credit from the address that actually won. Both halves are\n\t\t\t// captured together because they answer one question: who won, and\n\t\t\t// with how much of their own capital.\n\t\t\tcs.overturnBy = cs.disputer\n\t\t\tcs.overturnBond = posted\n\t\t}\n\t\tif posted \u003e 0 {\n\t\t\tc.coin.Transfer(c.escrow, cs.disputer, posted)\n\t\t\tcs.disputeBond = 0\n\t\t}\n\t\tif comp := compAmount(posted, burned); comp \u003e 0 {\n\t\t\tenqueueSenior(c, cs.disputer, comp, \"comp\")\n\t\t}\n\t\tcs.provisional = int8(oppositeSide(int(cs.answer)))\n\t\tcs.failedRounds = 0\n\t\tcs.decidedRounds++\n\t\tcs.decidedPID, cs.carrotChoice, cs.carrotDenom = cs.proposalID, \"yes\", yes\n\t\t// The spam share of THIS round, frozen with the rest of the deciding\n\t\t// round's record. cast is yes+no+spam — the same denominator the quorum\n\t\t// bar was measured against, so the discount is a fraction of the turnout\n\t\t// that actually decided the claim.\n\t\tcs.spamW, cs.spamTotalW = spamW, cast\n\tdefault:\n\t\t// UPHOLD (tie, all-abstain, or a sub-threshold split, quorum met):\n\t\t// the disputer's bond BURNS in full; the answerer is comped against\n\t\t// their posted bond magnitude (answerBond0 — the live field may\n\t\t// already be spent by an earlier overturn round). Their bond stays\n\t\t// escrowed until VERDICT_FINAL: still at stake for reopens. A pending\n\t\t// slash is left carved here — comp reads answerBond0, so the reserve\n\t\t// changes no arithmetic in this branch — and is disposed below by this\n\t\t// round's own ride (v0.47).\n\t\tburned := cs.disputeBond\n\t\tif burned \u003e 0 {\n\t\t\tc.coin.Burn(c.escrow, burned)\n\t\t\tcs.disputeBond = 0\n\t\t}\n\t\tif comp := compAmount(cs.answerBond0, burned); comp \u003e 0 {\n\t\t\tenqueueSenior(c, cs.answerer, comp, \"comp\")\n\t\t}\n\t\t// The credential bar (audit M2-1): this uphold counts toward the\n\t\t// difficulty record only if the OVERTURN side carried real weight —\n\t\t// \u003e= 1/4 of the (supply-floored) quorum floor. A manufactured contest\n\t\t// has no adversarial weight unless the farmer HOLDS and votes ~1.25%\n\t\t// of court supply against their own claim (the v0.28 pricing:\n\t\t// supply-absolute prizes demand supply-absolute weight).\n\t\t//\n\t\t// \"HOLDS\" IS ENFORCED NOW, and it was aspirational when written: before\n\t\t// the vote floor the farmer could rent the weight, vote it against their\n\t\t// own claim, and return it the same hour. `yes` here is c.gov.Tally, and\n\t\t// the governor's tally is the CAPPED sum — VoteDispute hands it a ceiling\n\t\t// of the voter's live balance, so weight that has been given back\n\t\t// contributes nothing to this bar. Both sides read the proposal's own\n\t\t// epoch, so the comparison is coherent.\n\t\t//\n\t\t// The direction is also the safe one. Capping can only LOWER `yes`, so it\n\t\t// makes this bar harder to clear, and clearing it is what unlocks an\n\t\t// aggressive meta verb (meta.gno) and the difficulty earning below. A\n\t\t// floor that made the bar EASIER would be the dangerous rounding; this\n\t\t// one withholds.\n\t\t//\n\t\t// It must stay a read of the TALLY and never a re-derivation from\n\t\t// PastVotes. openrewards.gno's carrot had exactly that bug — re-deriving\n\t\t// a numerator the tally had capped — and over-paid every capped voter\n\t\t// until it was made to read the recorded figure instead. A mutation row\n\t\t// pins this one, caught by TestMetaLifecycleGuards.\n\t\tif yes \u003e= credWeightFloor(c, cs.proposalID)/4 {\n\t\t\tcs.credEligible = true\n\t\t}\n\t\tcs.provisional = cs.answer\n\t\tcs.failedRounds = 0\n\t\tcs.decidedRounds++\n\t\tcs.decidedPID, cs.carrotChoice, cs.carrotDenom = cs.proposalID, \"no\", no\n\t\t// The spam share of THIS round, frozen with the rest of the deciding\n\t\t// round's record. cast is yes+no+spam — the same denominator the quorum\n\t\t// bar was measured against, so the discount is a fraction of the turnout\n\t\t// that actually decided the claim.\n\t\tcs.spamW, cs.spamTotalW = spamW, cast\n\t}\n\n\t// The escrow clock is set ONCE, at the first resolution, and never\n\t// recomputed — a reopen does not reset it (V1 §3.6).\n\tif firstResolution \u0026\u0026 !cs.provClose {\n\t\tw := escrowWindow(c, cs)\n\t\t// S2 — A QUORUM-LESS VERDICT MUST NOT FINALIZE BY APATHY.\n\t\t//\n\t\t// votingBlocks and escrowMinBlocks are the same number, and escrowWindow\n\t\t// returns the floor whenever extraDays rounds to zero — so round 3 could not\n\t\t// open, failedRounds capped at 2 against a maxFailedRounds of 3, and\n\t\t// provCloseClaim was unreachable on any court without a curve price. Meanwhile\n\t\t// the FIRST failed round has already written provisional = answer. So apathy\n\t\t// resolved the claim in the liar's favour: the outcome the failed-quorum\n\t\t// branch's own comment forbids. Three shipped fixtures widen this window\n\t\t// in-test to reach provClose, one of them naming the bug.\n\t\t//\n\t\t// Floored ONLY when the standing verdict is a quorum-less DEFAULT. A decided\n\t\t// first round keeps today's window bit-identically — raising escrowMinBlocks\n\t\t// globally instead would tax an honest DISPUTED claim from 14 to 21 days of\n\t\t// frozen winning-side principal, which is the objection used to reject a\n\t\t// longer settle window, and it would double the reopen grind chain.\n\t\t//\n\t\t// BOTH ARMS. The gates read escrowUntilAt whenever the stamp exists, so\n\t\t// flooring only the height arm leaves the bug fully live. Computing w once is\n\t\t// also what stops the two stamps drifting apart.\n\t\t//\n\t\t// KNOWN AND NOT CLOSED: an abstain-only round with quorum met is not a FAILED\n\t\t// round — it upholds — so one free abstain from a non-participant keeps\n\t\t// failedRounds at zero and denies this floor for the claim's whole life. That\n\t\t// costs the abstainer nothing. So this defeats APATHY, not an active adversary,\n\t\t// and it is strictly better than today either way.\n\t\tif cs.failedRounds \u003e 0 {\n\t\t\tif lw := ladderWindow(c.params); lw \u003e w {\n\t\t\t\tw = lw\n\t\t\t}\n\t\t}\n\t\tcs.escrowUntil = heightNow() + w\n\t\tcs.escrowUntilAt = nowTime() + blocksToSecs(w)\n\t}\n\tcs.disputeOpen = false\n}\n\n// disputeBond0 is the dispute bond at the claim's current failed-round count: the\n// ONE definition, quoted by DisputeBondNext and the claim page and CHARGED by\n// OpenDispute.\n//\n// It said \"OpenDispute's formula, factored for DisputeBondNext\" while OpenDispute\n// went on computing the same eleven lines itself, byte for byte. That is a quote\n// that can drift from the charge — the shape check-epoch-coherence's one-weight arm\n// exists to prevent for vote weight, unguarded here for money. What drift would look\n// like: render.gno tells the user \"N CC units (doubles per failed round)\" and\n// DisputeBondNext returns N, and then OpenDispute takes something else.\nfunc (cs *claimState) disputeBond0() int64 {\n\tbase := cs.xBarFrozen * disputeBondXBps / 10000\n\tif arm := cs.answerBond0 * disputeBondOfAnswerBps / 10000; arm \u003c base {\n\t\tbase = arm\n\t}\n\tif base \u003c 1 {\n\t\tbase = 1\n\t}\n\tshift := cs.failedRounds\n\tif shift \u003e maxFailedRounds-1 {\n\t\tshift = maxFailedRounds - 1\n\t}\n\treturn mustMul(base, int64(1)\u003c\u003cuint(shift))\n}\n\n// compAmount is the doctrine's cap pair: min(2 × own bond, floor(80% × the\n// loser's burn)) — tier-invariant, never negative (v0.20/v0.25).\nfunc compAmount(ownBond, burned int64) int64 {\n\tcomp := mustMul(compOwnX, ownBond)\n\tif arm := burned * compOfBurnBps / 10000; arm \u003c comp {\n\t\tcomp = arm\n\t}\n\tif comp \u003c 0 {\n\t\tcomp = 0\n\t}\n\treturn comp\n}\n\n// spamDiscardClaim ends a claim the electorate threw out: more than half the\n// weight cast called it spam.\n//\n// ONLY REACHABLE WITH QUORUM MET, because it is a case in ResolveDispute's\n// switch AFTER the failed-quorum arm. That ordering is the whole protection\n// against a three-voter deletion — the discard is a fraction of a turnout that\n// already had to clear min(X̄, votable/3).\n//\n// THE AUTHOR IS THE ONLY PARTY WHO LOSES, which is where the spam came from:\n//\n//\tstakes, both sides   returned 1x   (provClose opens the same withdrawals)\n//\tanswerer's bond      returned      they did the work; the junk is not theirs\n//\tdisputer's bond      returned      they opened the round that FOUND it —\n//\t                                   burning it would discourage the only path\n//\t                                   that surfaces spam at all\n//\tthe fee              always burns  it is the anti-spam price and this is the\n//\t                                   case it was priced for\n//\tthe deposit          burns at 2/3  a supermajority, not a bare majority: the\n//\t                                   deposit is the harshest money consequence\n//\t                                   in this system reachable by somebody who\n//\t                                   posted no bond, so it wants a wider margin\n//\t                                   than the discard itself\n//\n// It carries provClose as well as spamClosed. That is not sloppiness: provClose\n// is what every withdrawal path already reads to mean \"everyone takes their\n// principal back\", and reusing it is how the stake half of this needs no new\n// code. spamClosed is the REASON, so a page can tell a claim nobody turned up\n// for from one the court looked at and threw out.\n//\n// No draw is written and none is possible: the claim's own spam share is past\n// half, so spamNetBps is zero and open the rewards multiplies the draw to nothing.\n// Belt and braces, in opposite directions — the money is refused here and the\n// arithmetic refuses it again there.\n// spamDiscards and spamBurnsDeposit are the two thresholds, as predicates.\n//\n// EXTRACTED so they can be tested without a claim. They are integer comparisons\n// with no division — a fraction here would round, and rounding at a threshold is\n// how \"exactly half\" ends up on the wrong side of a line the owner drew.\n//\n// The pair is deliberately asymmetric: the discard is a bare majority, and the\n// deposit burn wants a supermajority. Burning an author's deposit is the harshest\n// money consequence in this system reachable by somebody who posted no bond, so\n// it does not ride on the same margin that merely deletes the claim.\nfunc spamDiscards(spamW, cast int64) bool { return spamW*2 \u003e cast }\n\nfunc spamBurnsDeposit(spamW, cast int64) bool { return spamW*3 \u003e= cast*2 }\n\nfunc spamDiscardClaim(c *Court, cs *claimState, spamW, cast int64) {\n\tcs.spamClosed = true\n\tcs.provClose = true\n\tcs.verdictAt = heightNow()\n\tcs.verdictAtTime = nowTime()\n\tcs.route = \"spam\"\n\tcs.spamW, cs.spamTotalW = spamW, cast\n\t// THE CARROT PAYS THE SIDE THAT WON, and here that side is spam.\n\t//\n\t// Without this the spam button is dominated exactly as abstain was — same\n\t// coin locked, same turnout added, no upside — and no payoff-maximiser ever\n\t// presses it. A flag nobody rationally raises is not a signal, so this line\n\t// is what makes the button real rather than decorative.\n\t//\n\t// It needs no new record. VoteDispute already writes each voter's choice in\n\t// the COURT's vocabulary under \"d\"+pid+addr, and PullCarrot compares that\n\t// against carrotChoice — so writing voteSpam here is the whole wiring. That\n\t// is the one-character seam step 1 was careful about.\n\t//\n\t// decidedPID is set for the same reason the verdict arms set it: PullCarrot\n\t// reads the DECIDING round's record, and cs.proposalID is overwritten by any\n\t// later round. A discarded claim has no later round, but the read is shared\n\t// and must not depend on that.\n\tcs.decidedPID, cs.carrotChoice, cs.carrotDenom = cs.proposalID, voteSpam, spamW\n\n\tif cs.answerBond \u003e 0 {\n\t\tsettleAnswerBond(c, cs)\n\t}\n\tif cs.disputeBond \u003e 0 {\n\t\tc.coin.Transfer(c.escrow, cs.disputer, cs.disputeBond)\n\t\tcs.disputeBond = 0\n\t}\n\t// THE SUPERMAJORITY ARM, in integers and on the same numbers the discard\n\t// used: two thirds of the weight cast, not two thirds of the spam.\n\tburnDeposit := spamBurnsDeposit(spamW, cast)\n\tif cs.deposit \u003e 0 {\n\t\tif burnDeposit {\n\t\t\tc.coin.Burn(c.escrow, cs.deposit)\n\t\t} else {\n\t\t\tc.coin.Transfer(c.escrow, cs.author, cs.deposit)\n\t\t}\n\t\tc.deposit -= cs.deposit\n\t\tcs.deposit = 0\n\t}\n\tif cs.fee \u003e 0 {\n\t\tc.coin.Burn(c.escrow, cs.fee)\n\t\tc.deposit -= cs.fee\n\t\tcs.fee = 0\n\t}\n\treleasePriority(c, cs.answerer, cs.id)\n\tstripExit(c, cs)\n\treleaseMetaLatchIfMeta(c, cs)\n}\n\n// provCloseClaim closes a claim without a decision after three failed rounds:\n// everyone 1× (withdrawals open), no draw ever, the answer bond returns, the\n// deposit AND fee refund (provClose is not a conclusive low — §3.1.7).\nfunc provCloseClaim(c *Court, cs *claimState) {\n\tcs.provClose = true\n\tcs.verdictAt = heightNow()\n\tcs.verdictAtTime = nowTime()\n\tcs.route = \"closed\"\n\tif cs.answerBond \u003e 0 {\n\t\tsettleAnswerBond(c, cs)\n\t}\n\tif cs.deposit \u003e 0 {\n\t\tc.coin.Transfer(c.escrow, cs.author, cs.deposit)\n\t\tc.deposit -= cs.deposit\n\t\tcs.deposit = 0\n\t}\n\tif cs.fee \u003e 0 {\n\t\tc.coin.Transfer(c.escrow, cs.author, cs.fee)\n\t\tc.deposit -= cs.fee\n\t\tcs.fee = 0\n\t}\n\treleasePriority(c, cs.answerer, cs.id)\n\t// A provClosed claim is terminal — unflaggable and undisputable — so it is\n\t// no longer policeable and must leave the strips, or its row would linger\n\t// forever (§5.3 membership exit).\n\tstripExit(c, cs)\n\treleaseMetaLatchIfMeta(c, cs)\n}\n\n// Finalize makes a disputed claim's standing provisional verdict FINAL once\n// the escrow window has passed with no active dispute: withdrawals open for\n// both sides, the answer bond is disposed (returned if the answer stands — it\n// burned already if not, LESS a slash-sized reserve while the claim is still\n// slash-eligible, mirroring SettleUndisputed's M3-HIGH-1 retention), and the\n// answer record moves once, from the final verdict (P13). Participant-only for\n// the first week of eligibility (A13), permissionless after. Deposit, fee, and\n// any retained reserve wait for QUALITY_FINAL (open the rewards).\nfunc Finalize(cur realm, courtSlug string, claimID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif cs.verdictAt != 0 {\n\t\tpanic(\"kourtv2: already final\")\n\t}\n\tif cs.disputeOpen {\n\t\tpanic(\"kourtv2: a dispute is active; cannot finalize\")\n\t}\n\tif cs.provisional \u003c 0 {\n\t\tpanic(\"kourtv2: no provisional verdict to finalize\")\n\t}\n\tnow := heightNow()\n\tif !escrowPassed(cs) {\n\t\tpanic(\"kourtv2: the escrow window has not passed\")\n\t}\n\t/* THE TWIN OF openrewards.gno's GATE, and it drifts the same way: counted in\n\t   blocks the week outlasts itself on any chain slower than 5s, so a claim\n\t   parked in 2022 was still refusing strangers years later. escrowUntilAt is\n\t   already a FUTURE stamp (set beside escrowUntil above), so the grace runs\n\t   from it directly. Blocks only for a claim parked before the stamp existed. */\n\tif passed, known := pastDeadline(cs.escrowUntilAt, finalizeGraceSecs); ((known \u0026\u0026 !passed) ||\n\t\t(!known \u0026\u0026 now \u003c cs.escrowUntil+finalizeGraceBlocks)) \u0026\u0026 !isParticipant(cs, who) {\n\t\tpanic(\"kourtv2: Finalize is participant-only for its first week\")\n\t}\n\ttouch(c)\n\n\tcs.verdictAt = now\n\tcs.verdictAtTime = nowTime()\n\tcs.route = \"vote\"\n\t// Clip the frozen record's hourly tail to its final week (stakeseries.gno);\n\t// no later stake op will ride a trim past this point.\n\ttrimClaimHistory(c, cs, uint32(cs.frozenAt/epochBlocks)+1)\n\tif cs.answerBond \u003e 0 {\n\t\t// The answer stood — an overturned bond was burned at its round, so\n\t\t// reaching here with a bond means it was never forfeited. It returns\n\t\t// WHOLE; why whole rather than partly retained is settleAnswerBond's\n\t\t// story (answerbond.gno), not repeated here.\n\t\t//\n\t\t// ONE PREDICATE, SHARED, and this part is live: the gate is\n\t\t// character-identical to SettleUndisputed's, which is the v0.44 symmetry\n\t\t// goal and the reason settleAnswerBond exists at all rather than the same\n\t\t// two lines appearing on both terminal paths. v0.43's proof that an\n\t\t// overturn cannot retain rests on the enclosing `answerBond \u003e 0` alone.\n\t\t//\n\t\t// WHAT USED TO BE HERE, kept as a record because a bare deletion invites\n\t\t// its return: twenty-four lines sizing a slash-sized reserve to hold past\n\t\t// Finalize (M3-HIGH-1, v0.47), on the reasoning that votingBlocks exceeds\n\t\t// the dispute and escrow windows so a conclusive-low flag would resolve\n\t\t// after this point and find a bond already returned. It reasoned through\n\t\t// quality.gno's gate, slotConsumed, OpenFlag and a pending slash — the\n\t\t// lane those belong to is gone, and none of those symbols has a\n\t\t// non-comment line left in this realm. It also claimed to \"retain a\n\t\t// slash-sized reserve exactly as SettleUndisputed does\", which stopped\n\t\t// being true when that path stopped retaining; its twin note was corrected\n\t\t// in 76b2941. decidedRounds, which the old text also named, IS still live\n\t\t// — it is the gate that went, not the counter.\n\t\tsettleAnswerBond(c, cs)\n\t}\n\t// The record moves once, from the final verdict (P13) — and \"upheld\"\n\t// means a DECIDED vote upheld it AGAINST real opposition: quorum-less\n\t// defaults earn nothing, and so do weightless contests (credEligible,\n\t// audit M2-1) — else ~20% of a dispute bond buys a difficulty point on\n\t// any trivially-true claim.\n\tif cs.provisional != cs.answer {\n\t\tresetOverturned(c, cs.answerer)\n\t} else if cs.decidedRounds \u003e 0 \u0026\u0026 cs.credEligible {\n\t\tcreditUpheld(c, cs.answerer) // contested-and-upheld: the hard point\n\t}\n\t// Standing rides the SAME predicate one ledger over (standing.gno), and\n\t// deliberately so — an uphold earns only when it was credEligible, because\n\t// otherwise ~20% of a dispute bond buys standing on any trivially-true claim,\n\t// which is the M2-1 finding this branch already encodes for the record.\n\t// The two ledgers stay separate: this one is slashable, the record is not.\n\tcreditDisputeResolved(c, cs)\n\treleasePriority(c, cs.answerer, claimID)\n}\n\n// VotableSupply reports a court's coin supply at the last sealed epoch, split\n// three ways: raw is the whole supply, escrowed is the weight sitting in the\n// court escrow, and votable is raw minus escrowed — the base every percentage\n// bar in this court is quoted against.\n//\n// VOTABLE IS AN UPPER BOUND ON WHAT CAN ACTUALLY VOTE, not the figure itself.\n// It nets out the court escrow and nothing else. Coin parked at a wrapper's\n// address is inside `votable` while no key can vote it, so the castable total is\n// `votable` minus whatever the wrapper holds. A client that knows the wrapper\n// can subtract CoinBalanceOf(courtSlug, wrapperAddress) itself; this court does\n// not, deliberately, so that no safety bar here depends on that package being\n// correct. r/kourt/ccwrap caps its vault at 35% of votable, so the overstatement\n// is bounded at 35% while that cap holds.\n//\n// Read it as the health of a court's dispute lane: a court where escrowed is\n// most of raw has little weight left to vote on the claims that weight is staked\n// against, and that reads from outside as apathy rather than as a fault.\n//\n// Answers 0, 0, 0 before the first epoch seals rather than refusing. Refused\n// only on a court that does not exist.\nfunc VotableSupply(courtSlug string) (raw, escrowed, votable int64) {\n\tc := mustCourt(courtSlug)\n\tat := c.coin.Epoch()\n\tif at \u003c= 1 {\n\t\treturn 0, 0, 0\n\t}\n\tat--\n\traw = c.coin.PastTotal(at)\n\tescrowed = c.coin.PastVotes(c.escrow, at)\n\tvotable = raw - escrowed\n\tif votable \u003c 0 {\n\t\tvotable = 0\n\t}\n\treturn raw, escrowed, votable\n}\n\n// QuorumFloorOf quotes the turnout a dispute on this claim must reach for the\n// vote to decide anything, in CC base units.\n//\n// WHILE A DISPUTE IS OPEN THIS IS THE BAR THAT WILL ACTUALLY BE ENFORCED — the\n// figure frozen into that round when it opened, not a fresh computation. With no\n// dispute open it quotes what a dispute opened now would face, which moves with\n// supply and can be several times smaller. Mobilise against the number this\n// returns while the round is live.\n//\n// The bar exists to stop a thin round deciding a claim, and missing it is not\n// neutral: a dispute that fails for turnout leaves the provisional verdict equal\n// to the ANSWER, so falling short hands the claim to the answerer.\n//\n// Refused when the court or the claim does not exist.\nfunc QuorumFloorOf(courtSlug string, claimID uint64) int64 {\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif cs.disputeOpen {\n\t\treturn c.gov.QuorumFloor(cs.proposalID)\n\t}\n\treturn quorumFloor(c, cs.xBarFrozen)\n}\n\n// isParticipant: the claim's author, answerer, or any address with a staker\n// record (records persist across withdrawal — conviction holders count).\nfunc isParticipant(cs *claimState, who address) bool {\n\tif who == cs.author || who == cs.answerer {\n\t\treturn true\n\t}\n\treturn cs.stakers.Get(posKey(who, sideYES)) != nil ||\n\t\tcs.stakers.Get(posKey(who, sideNO)) != nil\n}\n\n// quorumFloor is the court's absolute turnout bar: max(5%·supply,\n// min(X̄frozen, votable/3)), at the last sealed epoch; votable nets out the\n// escrow so staked capital cannot inflate the denominator. Never zero.\nfunc quorumFloor(c *Court, xbar int64) int64 {\n\tat := c.coin.Epoch() - 1\n\tsupply := c.coin.PastTotal(at)\n\tvotable := supply - c.coin.PastVotes(c.escrow, at)\n\tif votable \u003c 0 {\n\t\tvotable = 0\n\t}\n\tfloor := xbar\n\tif third := votable / 3; third \u003c floor {\n\t\tfloor = third\n\t}\n\t// THE SUPPLY ARM AND ITS CLAMP ARE GONE (S1). Both were identities under the\n\t// only condition worth keeping, and together they handed small claims to the\n\t// party the bar exists to police.\n\t//\n\t// The bar was max()ed against 5% of court SUPPLY while the prize a claim pays is\n\t// denominated in that claim's own STAKE. Below X̄ = 5%·supply no turnout\n\t// proportional to the claim could clear it — 6.6x short at 1% of supply — and an\n\t// unreachable quorum does not mean \"no verdict\": the failed-quorum branch leaves\n\t// provisional = answer, burns half the disputer's bond, and returns the answer\n\t// bond whole. So non-satisfaction was a WIN for the answerer, and raising the bar\n\t// could not make that safer.\n\t//\n\t// Deleted rather than gated: under the gate that keeps them honest\n\t// (xbar \u003e= fivePct) both blocks are provable identities — the arm can only bite\n\t// when 5%·supply exceeds votable/3, and the clamp then restores votable/3, which\n\t// min(X̄, votable/3) already equalled. Verified analytically and measured over\n\t// 3.9M rows including an exhaustive sweep of [0,120]^3: zero divergences. Gating\n\t// them would have left two unreachable blocks and a page of prose describing\n\t// behaviour that no longer runs.\n\t//\n\t// WHAT THIS DOES NOT DO, because the honest framing matters: it does not let the\n\t// robbed pool defend itself. VoteDispute refuses any address with a staker row on\n\t// either side, there is no stakers.Remove anywhere in this realm, and a staker who\n\t// withdraws in full before the answer still carries the row — measured, a pool\n\t// holding 5x the floor was refused at the door. This lowers a bar only\n\t// NON-PARTICIPANTS can clear. What it buys is that a verdict becomes reachable at\n\t// all, by third parties, instead of defaulting to the answer.\n\t//\n\t// And it is symmetric: it drops the price of deciding an arbitrary small claim's\n\t// verdict from 5% of supply to the answerability floor's 0.10%, so the same\n\t// relaxation makes a malicious overturn reachable by those same third parties —\n\t// measured at −4.80 CC to +19.20 CC, and SIMULTANEOUS across every concurrently\n\t// open claim, since voting consumes no weight. That trade is accepted because the\n\t// snipe it fixes is currently FREE while a false overturn costs a bond.\n\t//\n\t// THE VOTE LOCK DID NOT CLOSE THIS, and saying so matters more than the lock's\n\t// good news. The lock commits the coin voted with, but voteLockedOf takes the MAX\n\t// across a holder's open questions, not the sum — one pile of coin still votes in\n\t// every concurrently open claim, and it must, because the two locks a holder can\n\t// carry overlap rather than stack. Measured, in\n\t// TestTwoQuestionsCommitOnePileOnce: two questions, one commitment.\n\t//\n\t// So the simultaneity survives exactly as written. What the lock adds is a CARRY\n\t// cost rather than a per-claim cost: the attacker holds that one pile through the\n\t// longest round it voted in, roughly a week, instead of voting everywhere and\n\t// selling in the next block. The return still scales with the number of open\n\t// claims; it is now earned against a week of price risk on the whole position.\n\t// A reader arriving from VOTEFLOOR.md should not infer more than that.\n\t//\n\t// THE 5% ANCHOR IS KEPT ELSEWHERE, and the three places do NOT share a\n\t// denominator. This list used to say they did, which matters because a base\n\t// mismatch is the exact bug class that has already bitten twice here: the\n\t// election bond was quoted on supply against a floor on votable, and ccwrap's\n\t// vault sat inside votable while no key could vote it. Read off the code:\n\t//\n\t//   election floor      5% of VOTABLE (modvote.gno electionFloor)\n\t//   quality full bar    5% of SUPPLY, then clamped down to votable/3 — the\n\t//                       clamp is what keeps it reachable, not the base\n\t//   credWeightFloor     5% of ENGAGED, and EngagedTotal == PastTotal, so RAW\n\t//                       SUPPLY with no votable clamp at all\n\t//\n\t// credWeightFloor is the outlier and it is deliberate. It gates a POWER —\n\t// credEligible needs weight \u003e= floor/4 voted AGAINST before an aggressive meta\n\t// verb executes — so a larger floor WITHHOLDS the verb. Measured at two fifths\n\t// escrowed: 5.00e9 on supply against 3.00e9 on votable, so the supply base is\n\t// 1.67x stricter, and it grows stricter as escrow grows. Re-basing it on\n\t// votable \"for consistency\" would make an aggressive override that much cheaper\n\t// to unlock, which is what TestCredWeightFloorIsQuotedOnSupplyNotVotable stops.\n\t//\n\t// Note check-demo-physics reads quorumSupplyBps out of this file BY NAME.\n\t// LAST, deliberately: governor.ProposeWithQuorum refuses a non-positive\n\t// floor, and a fully-escrowed court has votable == 0.\n\tif floor \u003c 1 {\n\t\tfloor = 1\n\t}\n\treturn floor\n}\n\nfunc oppositeSide(side int) int {\n\tif side == sideYES {\n\t\treturn sideNO\n\t}\n\treturn sideYES\n}\n\n// escrowWindow is how long a provisional verdict stays reopenable before\n// Finalize can land: clamp(1 week + 1 day per 500 GNOT-equivalent of X̄frozen,\n// escrowMin, escrowMax) — V1's rule with X̄frozen as V2's size proxy (the\n// collateral is gone). X̄frozen is immutable after the answer, so the window\n// cannot be shortened by manipulation; the 1-week floor bounds it regardless.\nfunc escrowWindow(c *Court, cs *claimState) int64 {\n\textraDays := mulDiv128(cs.xBarFrozen, c.crv.Price(c.minted), 500_000_000)\n\troom := (c.params.escrowMaxBlocks - c.params.escrowMinBlocks) / oneDayBlocks\n\tif extraDays \u003e= room {\n\t\treturn c.params.escrowMaxBlocks\n\t}\n\treturn c.params.escrowMinBlocks + extraDays*oneDayBlocks\n}\n\n// The dispute ballot's three choices, in the COURT's vocabulary.\n//\n// voteSpam is the governor's abstain bucket under a name that says what it does\n// here. Quoted as constants because the same three strings are compared in\n// VoteDispute, recorded in voted and matched by PullCarrot, and a typo in any\n// one of them is a vote that silently never earns its carrot.\nconst (\n\tvoteYes  = \"yes\"\n\tvoteNo   = \"no\"\n\tvoteSpam = \"spam\"\n\n\t// govAbstain is the p/ layer's word for the same bucket. Named so the one\n\t// translation point in VoteDispute reads as a translation rather than as a\n\t// magic string, and so nothing else in this realm reaches for it by accident.\n\tgovAbstain = \"abstain\"\n)\n\n// oneDayBlocks is a day at the five-second cadence, the escrow window's unit.\nconst oneDayBlocks = int64(24) * epochBlocks\n\n// ------------------------------------------------------------------ reads --\n\nfunc DisputeOpen(courtSlug string, claimID uint64) bool {\n\treturn mustClaim(mustCourt(courtSlug), claimID).disputeOpen\n}\n\nfunc DisputeProposal(courtSlug string, claimID uint64) int64 {\n\treturn mustClaim(mustCourt(courtSlug), claimID).proposalID\n}\n\n// Provisional is the standing (reopenable) verdict: sideYES/sideNO, -1 none.\nfunc Provisional(courtSlug string, claimID uint64) int {\n\treturn int(mustClaim(mustCourt(courtSlug), claimID).provisional)\n}\n\nfunc FailedRounds(courtSlug string, claimID uint64) int64 {\n\treturn mustClaim(mustCourt(courtSlug), claimID).failedRounds\n}\n\nfunc ProvClose(courtSlug string, claimID uint64) bool {\n\treturn mustClaim(mustCourt(courtSlug), claimID).provClose\n}\n\nfunc EscrowUntil(courtSlug string, claimID uint64) int64 {\n\treturn mustClaim(mustCourt(courtSlug), claimID).escrowUntil\n}\n\n// DisputeVoteCloses is the height this claim's open dispute round stops taking\n// votes, or 0 when no round is open.\n//\n// The governor has always known it — Timings(id) returns (opened, closes,\n// ready, expires) and closes is a HEIGHT, set at propose() as\n// `now + rules.VotingBlocks` off the same realmClock every deadline here uses.\n// What was missing is a way to ask kourtv2 for it, so a client showing \"voting\n// closes in four days\" had nothing to compute from: the overlay carried the\n// close by hand for its sample data and said, in live mode, that no read\n// published one. This is that read.\n//\n// A HEIGHT, and no longer because it is what the governor gates on — the\n// governor gates on a STAMP now (p/governor, closesTime). This stays a height\n// because it is a positional field in the packed claim head, where a change of\n// unit is invisible to every reader, and because the height is still the honest\n// reference beside the deadline.\n//\n// THE DATE IS PUBLISHED, just not here: ClaimTimeline carries it as\n// `voteclose:\u003cunix\u003e:\u003cheight\u003e`, the channel that already pairs the two. A\n// consumer wanting to say \"closes in four days\" reads that and does not\n// estimate. The estimate is what this comment used to call the consumer's job,\n// and it was wrong by five years on a real claim page.\n//\n// 0 rather than a panic on a claim with no open round: a client asks this on\n// every claim page it draws, and \"no round is open\" is an answer, not an error.\nfunc DisputeVoteCloses(courtSlug string, claimID uint64) int64 {\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif !cs.disputeOpen || cs.proposalID == 0 {\n\t\treturn 0\n\t}\n\t_, closes, _, _ := c.gov.Timings(cs.proposalID)\n\treturn closes\n}\n\n// DisputeBondNext quotes what the NEXT dispute round costs right now — the\n// page's bond quote (0-guard: quotable even before an answer exists).\nfunc DisputeBondNext(courtSlug string, claimID uint64) int64 {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tif cs.frozenAt == 0 {\n\t\treturn 0\n\t}\n\treturn cs.disputeBond0()\n}\n\n// escrowPassed reports whether the reopen window has closed.\n//\n// THREE PLACES ASK THIS and they had three answers. OpenDispute and Finalize\n// each wrote the stamp-then-height test out longhand, in mirrored polarity; this\n// function's third caller, Reopenable, was still reading `heightNow() \u003c\n// cs.escrowUntil` and nothing else. So the READ a client gates its \"reopen\"\n// txlink on could say the window was open while OpenDispute refused, or hide the\n// link while OpenDispute would have accepted — a button that lies in both\n// directions, on the same claim, in the same block.\n//\n// Not a projection: it answers the same question as the gate, so it converts\n// with the gate rather than in the projection pass. What IS a projection here is\n// the block NUMBER the web prints beside it (\"Reopenable until block N\"), which\n// still needs escrowUntilAt.\n//\n// secs is 0 because escrowUntilAt IS the deadline, not an event to add a window\n// to — the shape the election's phase stamps use.\nfunc escrowPassed(cs *claimState) bool {\n\tpassed, known := pastDeadline(cs.escrowUntilAt, 0)\n\treturn (known \u0026\u0026 passed) || (!known \u0026\u0026 heightNow() \u003e= cs.escrowUntil)\n}\n\n// Reopenable reports whether the standing provisional verdict can still be\n// disputed again. A client gates the \"reopen\" txlink on this.\nfunc Reopenable(courtSlug string, claimID uint64) bool {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\treturn cs.provisional \u003e= 0 \u0026\u0026 cs.verdictAt == 0 \u0026\u0026 !cs.disputeOpen \u0026\u0026\n\t\t!escrowPassed(cs)\n}\n\n// ladderWindow is the window a DEFAULTED verdict needs so the failed-round ladder\n// can actually be walked: one votingBlocks per round after the first, plus a\n// resolution-latency allowance per round, plus one block.\n//\n// THE GRACE TERM IS A LATENCY BUDGET, NOT CONSERVATISM — do not \"simplify\" it away.\n// ResolveDispute is permissionless and escrowUntil is stamped once, so every block\n// between a vote closing and somebody cranking it is spent out of this window.\n// Measured: a round-2 resolution 34,559 blocks late still reaches provClose, and\n// 34,560 does not. At the bare arithmetic minimum that budget is ZERO — the ladder\n// would require a resolver in the closing block of every single round.\nfunc ladderWindow(p Params) int64 {\n\treturn (maxFailedRounds-2)*p.votingBlocks + (maxFailedRounds-1)*p.graceBlocks + 1\n}\n\n// credWeightFloor is the CREDENTIAL bar's own anchor: 5% of the court supply that\n// was actually engaged by this tally.\n//\n// It keeps the 5%-of-supply price the quorum floor is giving up, because the\n// credential prices a CAREER and must not become forgeable just because one claim's\n// bar fell. Without it the bar would follow the claim (X̄/4), a cut of 5% divided by\n// the claim's share of supply — 50x at the answerability floor — and two socks\n// totalling 1.125% of supply would mint a record.\n//\n// THE EPOCH IS THE PROPOSAL'S, and that is the whole point of routing through\n// Snapshot. The numerator `yes` was weighed at the proposal's snapshot epoch; reading\n// supply at resolve instead reads it ~168 epochs later, so the bar would move under a\n// frozen tally and anyone could mint inside the voting window to strip an honest\n// contested uphold of its credit — measured 2.43x skew. It cuts both ways: burns\n// during the window would LOWER the bar, and ResolveDispute is permissionless, so a\n// farmer would pick the block.\nfunc credWeightFloor(c *Court, pid int64) int64 {\n\tengaged, _ := c.gov.Snapshot(pid)\n\treturn mulDiv128(engaged, quorumSupplyBps, grc20votes.Bps)\n}\n\n// credWeightFloorAt is credWeightFloor's expression against an explicit epoch,\n// for asking what a FRESH round would be judged against. Split out so the test\n// that pins the denominator reads the shipped formula instead of a paraphrase of\n// it — a paraphrased bar cannot notice the shipped one being re-based.\nfunc credWeightFloorAt(c *Court, at uint32) int64 {\n\treturn mulDiv128(c.coin.EngagedTotal(at), quorumSupplyBps, grc20votes.Bps)\n}\n"},{"name":"electionrender.gno","body":"// The election, which rendered nowhere.\n//\n// THE THIRD INSTANCE OF ONE PATTERN. The comment board had a working route and\n// nothing linked to it; the filing tree had seven verbs and no page; and the\n// ballot has fourteen entrypoints — five verbs and nine reads — and no page at\n// all. In every case the writes were gated, tested and mutation-covered, the\n// read side existed, and only the rendering was missing. The suite proved the\n// machinery worked; nothing proved anybody could see it.\n//\n// WHY THIS ONE IS THE WORST OF THE THREE. A court's moderators can hide claims,\n// hide comments, slash standing, freeze addresses out of the boards and answer\n// claims. The election is the ONLY mechanism by which holders replace them. And\n// an election that closes under its floor does not void — `install` requires\n// `turnout \u003e= floor`, and below it the CURRENT set stands. So an election nobody\n// can see is not a missing feature, it is an incumbent-retention machine: the\n// nomination window passes with no candidates, the vote window passes with no\n// turnout, and the outcome is indistinguishable from a court that voted to keep\n// the moderators it had.\n//\n// The reads exist because somebody expected this page: ElectionTally's own\n// comment says it \"exposes a line's approving weight and retain's, for render\".\n\npackage kourtv2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\tsanitize \"gno.land/p/nt/markdown/sanitize/v0\"\n)\n\n// electionPath is the sub-route, spelled once. Not a slug collision risk: it\n// sits at parts[1], where \"mod\" and \"me\" already live, and a claim id is\n// numeric so a word can never be taken for one.\nconst electionPath = \"election\"\n\n// renderElection is /\u003cslug\u003e/election.\n//\n// IT RENDERS WHETHER OR NOT ONE IS OPEN, because \"no election is open\" is the\n// answer a holder most often needs and the state in which they can do something\n// about it. A page that 404s when the ballot is closed would tell somebody\n// looking for their remedy that the remedy does not exist.\nfunc renderElection(slug string) string {\n\tif !Exists(slug) {\n\t\treturn \"## Not found\\nNo court by that slug.\"\n\t}\n\tc := mustCourt(slug)\n\tbase := \"/r/kourt/kourtv2:\" + slug\n\tvar b strings.Builder\n\tb.WriteString(\"# Electing the moderators of \" + courtNameFor(c) + \"\\n\\n\")\n\tb.WriteString(\"[← \" + courtNameFor(c) + \"](\" + base + \") · [moderation log](\" +\n\t\tbase + \"/mod)\\n\\n\")\n\t// WHAT IS AT STAKE IN AN ELECTION, said before the mechanics. A reader who\n\t// does not already know what moderators can do cannot judge whether this\n\t// page is worth their attention, and the powers are not guessable from the\n\t// word \"moderator\".\n\tb.WriteString(\"A court's moderators answer claims, curate its folders, and can \" +\n\t\t\"hide a claim, hide a comment, slash somebody's standing, or freeze an \" +\n\t\t\"address out of the boards. **This ballot is the only way the court's \" +\n\t\t\"holders replace them.**\\n\\n\")\n\n\tcm := c.mod\n\tif cm == nil || cm.election == nil || cm.election.resolved {\n\t\twriteNoElection(\u0026b, c, cm)\n\t\tb.WriteString(\"\\n\" + helpLink + \"\\n\")\n\t\treturn b.String()\n\t}\n\twriteOpenElection(\u0026b, c, cm, base)\n\tb.WriteString(\"\\n\" + helpLink + \"\\n\")\n\treturn b.String()\n}\n\n// writeNoElection is the closed state: who holds the seats now, and what it\n// would take to challenge them.\nfunc writeNoElection(b *strings.Builder, c *Court, cm *courtMod) {\n\tb.WriteString(\"## No election is open\\n\\n\")\n\tif cm == nil || cm.n == 0 {\n\t\tb.WriteString(\"This court has no moderator set yet — its creator holds the \" +\n\t\t\t\"seat until an election or an appointment changes that.\\n\")\n\t} else {\n\t\tb.WriteString(\"- seats held now: **\" + strconv.Itoa(cm.n) + \"**, \" +\n\t\t\tstrconv.Itoa(cm.m) + \" of whom must agree for a moderation act\\n\")\n\t}\n\tnow := heightNow()\n\t// THE COOLDOWN, WITH ITS DEADLINE. A holder told only \"you cannot open one\"\n\t// has no way to learn when they can, and the number is already in state.\n\tif cm != nil \u0026\u0026 electionCooldownOpen(cm) {\n\t\tb.WriteString(\"- this court is in its post-election cooldown until block **\" +\n\t\t\tstrconv.FormatInt(cm.electionCooldownUntil, 10) + \"** (now \" +\n\t\t\tstrconv.FormatInt(now, 10) + \"); no ballot can open before then\\n\\n\")\n\t\treturn\n\t}\n\tb.WriteString(\"- a ballot **can be opened now**\\n\\n\")\n\t// The two steps, in order, because RegisterModCandidate must precede\n\t// OpenElection and the failure otherwise is a panic with no page to explain\n\t// it.\n\tb.WriteString(\"Opening one takes two calls: `RegisterModCandidate` records a \" +\n\t\t\"proposed set of moderators and returns its id, and `OpenElection` puts \" +\n\t\t\"that id on a ballot. Any holder may do both.\\n\\n\")\n}\n\n// writeOpenElection is the live ballot.\nfunc writeOpenElection(b *strings.Builder, c *Court, cm *courtMod, base string) {\n\te := cm.election\n\tnow := heightNow()\n\tb.WriteString(\"## A ballot is open\\n\\n\")\n\t// WHICH PHASE, FIRST. The two windows govern different actions, and a reader\n\t// who nominates during the vote window or votes during nomination gets a\n\t// panic. The phase is derived rather than left for the reader to compare two\n\t// block numbers against a third — and derived through the SAME predicates the\n\t// gates use, so the banner cannot say \"Nominating\" while the chain refuses a\n\t// nomination. The block numbers printed below are still heights: they are the\n\t// reference beside the deadline, and converting what they SAY is the\n\t// projection pass, not this one.\n\tswitch {\n\tcase !nominationClosed(e):\n\t\tb.WriteString(\"**Nominating.** Candidate sets may be added until block **\" +\n\t\t\tstrconv.FormatInt(e.nominateEnd, 10) + \"**; voting runs from then \" +\n\t\t\t\"until block **\" + strconv.FormatInt(e.voteEnd, 10) + \"**.\\n\\n\")\n\tcase !votingClosed(e):\n\t\tb.WriteString(\"**Voting.** Nominations closed at block \" +\n\t\t\tstrconv.FormatInt(e.nominateEnd, 10) + \"; the ballot closes at block **\" +\n\t\t\tstrconv.FormatInt(e.voteEnd, 10) + \"**.\\n\\n\")\n\tdefault:\n\t\tb.WriteString(\"**Closed and awaiting resolution.** The ballot shut at block \" +\n\t\t\tstrconv.FormatInt(e.voteEnd, 10) + \". Anyone may call `ResolveElection` \" +\n\t\t\t\"to apply the result below.\\n\\n\")\n\t}\n\tb.WriteString(\"- now: block \" + strconv.FormatInt(now, 10) + \"\\n\")\n\tb.WriteString(\"- a candidate set costs a bond of **\" +\n\t\tstrconv.FormatInt(e.bond, 10) + \" \" + qualifiedSymbol(c) + \"**\\n\")\n\tb.WriteString(\"- weights are pinned at the epoch the ballot opened, so buying \" +\n\t\t\"coin now does not add voting weight to it\\n\\n\")\n\n\twriteBallotLines(b, c, e)\n\twriteTurnoutBar(b, c, e)\n}\n\n// writeBallotLines is every candidate set on the ballot, with its members and\n// its approving weight.\n//\n// RETAIN IS A LINE LIKE ANY OTHER, listed beside the challengers rather than\n// buried under them: it is what wins if nobody clears the bar, so a reader\n// comparing the field needs it in the same list they are comparing.\nfunc writeBallotLines(b *strings.Builder, c *Court, e *election) {\n\tb.WriteString(\"## On the ballot\\n\\n\")\n\trows := 0\n\te.lines.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tl, ok := v.(*ballotLine)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\trows++\n\t\tb.WriteString(\"- **set #\" + strconv.FormatUint(l.candID, 10) + \"** — \" +\n\t\t\tstrconv.FormatInt(l.weight, 10) + \" \" + qualifiedSymbol(c) + \" approving\\n\")\n\t\tif cv := c.mod.candidates.Get(beClaimKey(l.candID)); cv != nil {\n\t\t\tcand := cv.(*modCandidate)\n\t\t\tb.WriteString(\"  - would seat \" + strconv.Itoa(len(cand.members)) +\n\t\t\t\t\", \" + strconv.Itoa(cand.m) + \" of whom must agree\\n\")\n\t\t\tfor _, m := range cand.members {\n\t\t\t\tb.WriteString(\"  - `\" + sanitize.InlineText(m.String()) + \"`\\n\")\n\t\t\t}\n\t\t}\n\t\treturn rows \u003e= renderPageSize\n\t})\n\tif rows == 0 {\n\t\tb.WriteString(\"_No candidate sets yet._ Until one is nominated there is \" +\n\t\t\t\"nothing to elect, and the current moderators stay.\\n\\n\")\n\t} else {\n\t\tb.WriteString(\"\\n\")\n\t}\n\tb.WriteString(\"- **keep the current moderators** — \" +\n\t\tstrconv.FormatInt(e.retainW, 10) + \" \" + qualifiedSymbol(c) + \" approving\\n\\n\")\n}\n\n// writeTurnoutBar is the floor, and the warning that goes with it.\n//\n// THE HAZARD THIS PAGE MOST HAD TO GET RIGHT. ElectionFloorOf's own comment says\n// it: install requires turnout \u003e= floor, and BELOW IT NOTHING INSTALLS AND THE\n// CURRENT SET STANDS. So an over-read of achievable turnout — mobilising to what\n// looks like the bar and falling short — hands the election to the incumbents.\n// Falling short is not a null result; it is a win for one side.\n//\n// And the floor is 5% of `votable`, which nets out the court escrow and NOTHING\n// ELSE: coin sitting at a wrapper's address is inside that pool and no key can\n// vote it. A reader who reasons \"we need 5% of the pool\" is reasoning about a\n// pool part of which cannot turn out, so the real share of votable-and-castable\n// weight required is higher than the percentage suggests. Both facts are printed\n// rather than left for a reader to derive, because deriving them wrong has a\n// direction and the direction favours the incumbents.\nfunc writeTurnoutBar(b *strings.Builder, c *Court, e *election) {\n\tb.WriteString(\"## Turnout\\n\\n\")\n\tb.WriteString(\"- cast so far: **\" + strconv.FormatInt(e.turnout, 10) + \" \" +\n\t\tqualifiedSymbol(c) + \"**\\n\")\n\tb.WriteString(\"- the bar: **\" + strconv.FormatInt(e.floor, 10) + \" \" +\n\t\tqualifiedSymbol(c) + \"**, fixed when the ballot opened\\n\")\n\tif e.turnout \u003c e.floor {\n\t\tb.WriteString(\"- still needed: **\" + strconv.FormatInt(e.floor-e.turnout, 10) +\n\t\t\t\" \" + qualifiedSymbol(c) + \"**\\n\\n\")\n\t} else {\n\t\tb.WriteString(\"- the bar is cleared\\n\\n\")\n\t}\n\tb.WriteString(\"\u003e **Falling short is not a draw.** If the ballot closes with less \" +\n\t\t\"than the bar cast, nothing is installed and the current moderators keep \" +\n\t\t\"their seats — the same outcome as a vote to retain them.\\n\u003e\\n\" +\n\t\t\"\u003e And the bar is a share of this court's coin **including coin that \" +\n\t\t\"cannot vote**: it nets out only the court's own escrow, so coin held at \" +\n\t\t\"a wrapper's address counts toward the pool the bar was measured against \" +\n\t\t\"while no key can cast it. The share of actually-castable weight you need \" +\n\t\t\"is therefore larger than the bar looks.\\n\\n\")\n}\n"},{"name":"emission.gno","body":"// Emission: the only mint besides the curve (PLAN §3.3, pins P5/P8/P10).\n//\n// Accrual: each block accrues b = B_period / periodBlocks with the remainder\n// carried exactly (Σ accrued over a period == the period's budget, to the\n// unit). B_period = S_live × the stepped-down budget bps, read once at each\n// period boundary — the OWNER's 20%/yr worst-case ceiling.\n//\n// Two lanes, one accrual line (P5):\n//\n//\tSENIOR — conduct comps, flag bounties, voter carrots: entitlements reserve\n//\tintervals [start, start+amount) on the cumulative-accrual number line at\n//\tenqueue time and are payable as accrual covers them — full amount, possibly\n//\ttime-delayed, never scaled (2A-T1).\n//\tJUNIOR — the tier-scaled claim draws: D = min(Σg, R) where R is accrual\n//\tpast the senior tail minus prior junior reservations; scaled once at\n//\topen the rewards, then pulled.\n//\n// The R_max pause (ceiling-not-floor): accrual stops while the junior pool is\n// full AND every senior is covered; the skipped budget is forgone forever.\n\npackage kourtv2\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n)\n\n// Senior-lane events (§11 legibility): a client indexes these to build a\n// per-address \"what am I owed / when do I get paid\" view WITHOUT any on-chain\n// walk of the queue (which would reintroduce the audit-N3 gas problem). Emits\n// carry no authority and cannot move funds — they mirror state already written.\nconst (\n\tseniorEnqueuedEvent = \"SeniorEnqueued\"\n\tseniorPaidEvent     = \"SeniorPaid\"\n)\n\n// entitlement is one senior claim on future accrual.\ntype entitlement struct {\n\tto      address\n\tamount  int64\n\tstart   int64 // position on the cumulative-accrual line (reservedTail at enqueue)\n\tpaid    int64\n\tpurpose string // \"comp\" | \"bounty\" | \"carrot\" — render/audit label\n}\n\n// touch advances a court's emission clock to now: walks period segments,\n// accruing with the carried remainder at each segment's budget, and rolls the\n// period (budget step-down, d_eff repricing) at each boundary. Every\n// state-changing entrypoint calls it first.\nfunc touch(c *Court) {\n\tnow := heightNow()\n\tfor c.lastAccrual \u003c now {\n\t\tboundary := c.createdAt + (c.periodIndex+1)*periodBlocks\n\t\tsegEnd := now\n\t\tif boundary \u003c segEnd {\n\t\t\tsegEnd = boundary\n\t\t}\n\t\taccrueSegment(c, segEnd-c.lastAccrual)\n\t\tadvanceRateAcc(c, segEnd-c.lastAccrual)\n\t\tc.lastAccrual = segEnd\n\t\tif segEnd == boundary {\n\t\t\trollPeriod(c)\n\t\t}\n\t}\n}\n\n// accrueSegment accrues `blocks` at the current period's budget, remainder\n// carried in units of 1/periodBlocks (P10: flooring never loses a unit across\n// a period). Skipped entirely while the R_max pause holds.\nfunc accrueSegment(c *Court, blocks int64) {\n\tif blocks \u003c= 0 || c.curPeriodBudget \u003c= 0 {\n\t\treturn\n\t}\n\t// `\u003e=` and not `\u003e`: AT the cap the segment is forgone. That operator is the\n\t// whole ceiling-not-floor policy and nothing visited its edge for a long time,\n\t// because natural accrual cannot land on the cap — it arrives past it and then\n\t// sits frozen while the step-down lowers rMax underneath. A junior draw of\n\t// exactly the overshoot is what occupies the edge;\n\t// TestTheReservoirPauseHoldsAtExactlyTheCap builds that state and pins both sides\n\t// of it.\n\t//\n\t// THE SECOND CONJUNCT IS IMPLIED BY THE FIRST, here and today. Above this line\n\t// curPeriodBudget \u003e 0, so rMax = rMaxPeriods x curPeriodBudget \u003e 0; the first\n\t// conjunct then needs reservoirR() \u003e 0, which needs cumAccrual \u003e reservedTail,\n\t// which is the second. So no test can distinguish `\u003e=` from `\u003e` in it — a probe\n\t// that panicked on the deciding state (first conjunct true AND cumAccrual ==\n\t// reservedTail) fired zero times across the whole realm suite. It stays because\n\t// rMaxPeriods is a DIAL: at 0 the first conjunct is vacuously true and this\n\t// becomes the only thing that keeps accruing for a senior who is still waiting.\n\tif c.reservoirR() \u003e= c.rMax() \u0026\u0026 c.cumAccrual \u003e= c.reservedTail {\n\t\treturn // full and no senior waits: the budget is forgone (ceiling, not floor)\n\t}\n\tnum := mustAdd(mustMul(c.curPeriodBudget, blocks), c.accrualRem)\n\tc.cumAccrual = mustAdd(c.cumAccrual, num/periodBlocks)\n\tc.accrualRem = num % periodBlocks\n}\n\n// advanceRateAcc extends the era integral by one segment at the segment's own\n// rate (audit H1): rateAcc is what conviction settles against — no position is\n// ever priced at a rate that wasn't live while it accrued.\nfunc advanceRateAcc(c *Court, blocks int64) {\n\t// UNREACHABLE and kept: the mutation harness reports this early return as a\n\t// survivor. `blocks` is always now-minus-lastTouch and touch() never runs\n\t// backwards, so it is never negative and a zero is already a no-op. It stays\n\t// because the failure it prevents is rateAcc moving DOWN, which would reprice\n\t// every live position's conviction — the same class H1 exists to stop, from the\n\t// other direction. Not claimed as verified; see PLAN §13 v0.66.\n\tif blocks \u003c= 0 {\n\t\treturn\n\t}\n\tc.rateAccFP = mustAdd(c.rateAccFP, mustMul(rateBpsFP(c), blocks))\n}\n\n// rollPeriod steps the budget down and reprices d_eff from realized dilution\n// (audit-15 P1): d_eff = min(budget ceiling, 4-period EMA of minted/supply).\nfunc rollPeriod(c *Court) {\n\tc.periodIndex++\n\tc.curBudgetBpsFP = c.curBudgetBpsFP * stepDownNum / stepDownDen\n\n\tminted := c.emittedTotal - c.emittedAtRoll\n\tc.emittedAtRoll = c.emittedTotal\n\tc.emaMinted = (3*c.emaMinted + minted) / 4\n\n\ts := c.coin.TotalSupply()\n\tif s \u003e 0 {\n\t\t// Also a harness survivor, and the source already called it what it is:\n\t\t// emittedTotal is monotone so this cannot fire. Fail-loud rather than\n\t\t// silent-wrong if that monotonicity is ever broken upstream.\n\t\tif c.emaMinted \u003c 0 {\n\t\t\tpanic(\"kourtv2: negative realized mint\") // emittedTotal is monotone; defensive\n\t\t}\n\t\t// 128-bit both ways (audit C1: the naive uint64 product wraps at\n\t\t// ~485k CC of supply — a silent 34× under-budget on any real court).\n\t\tdReal := mulDiv128(c.emaMinted, 10_000*1_000_000, s)\n\t\tif dReal \u003c c.curBudgetBpsFP {\n\t\t\tc.dEffBpsFP = dReal\n\t\t} else {\n\t\t\tc.dEffBpsFP = c.curBudgetBpsFP\n\t\t}\n\t\t// The period's budget: S_live × bps, floored (P10) — except a court on\n\t\t// an ABSOLUTE budget (meta), whose emission must not track a supply that\n\t\t// grows with other courts' burn (§13.8).\n\t\tif c.absBudget \u003e 0 {\n\t\t\tc.absBudget = c.absBudget * stepDownNum / stepDownDen\n\t\t\tc.curPeriodBudget = c.absBudget\n\t\t} else {\n\t\t\tc.curPeriodBudget = mulDiv128(s, c.curBudgetBpsFP, 10_000*1_000_000)\n\t\t}\n\t} else {\n\t\tc.dEffBpsFP = 0\n\t\tc.curPeriodBudget = 0\n\t}\n}\n\n// reservoirR is the junior pool: accrual past the senior tail, minus junior\n// reservations already made. Never negative.\nfunc (c *Court) reservoirR() int64 {\n\tpast := c.cumAccrual - c.reservedTail\n\tif past \u003c 0 {\n\t\tpast = 0\n\t}\n\tr := past - c.juniorReserved\n\tif r \u003c 0 {\n\t\tr = 0\n\t}\n\treturn r\n}\n\n// rMax banks at most rMaxPeriods of the CURRENT period's budget.\nfunc (c *Court) rMax() int64 { return rMaxPeriods * c.curPeriodBudget }\n\n// enqueueSenior reserves a senior entitlement (comp/bounty/carrot). Internal:\n// later modules call it at their decision points; nothing user-facing enqueues.\nfunc enqueueSenior(c *Court, to address, amount int64, purpose string) uint64 {\n\tif amount \u003c= 0 {\n\t\tpanic(\"kourtv2: a senior entitlement must be positive\")\n\t}\n\t// The start cursor must clear BOTH the prior senior tail AND every junior\n\t// draw already minted (audit M3-CRITICAL-1): juniors mint eagerly and\n\t// occupy accrual at [reservedTail, reservedTail+juniorReserved), but\n\t// reserveJunior does not advance reservedTail. A senior placed at the bare\n\t// reservedTail would overlap that stretch and PullSenior would re-pay\n\t// accrual the junior already minted, driving emittedTotal past cumAccrual\n\t// and breaking the ceiling. Placing it past juniorReserved keeps the whole\n\t// number line disjoint: seniors and juniors tile [0, reservedTail+juniorReserved)\n\t// without overlap. (Pre-open the rewards, juniorReserved==0 → unchanged.)\n\tstart := mustAdd(c.reservedTail, c.juniorReserved)\n\te := \u0026entitlement{to: to, amount: amount, start: start, purpose: purpose}\n\tc.reservedTail = mustAdd(c.reservedTail, amount)\n\tc.seniorOwed = mustAdd(c.seniorOwed, amount)\n\tc.queueSeq++\n\tc.queue.Set(beClaimKey(c.queueSeq), e)\n\tchain.Emit(seniorEnqueuedEvent,\n\t\t\"court\", c.id,\n\t\t\"seq\", strconv.FormatUint(c.queueSeq, 10),\n\t\t\"to\", to.String(),\n\t\t\"amount\", strconv.FormatInt(amount, 10),\n\t\t\"purpose\", purpose,\n\t)\n\treturn c.queueSeq\n}\n\n// reserveJunior reserves D from the junior pool at open the rewards time (later\n// modules). Clamps to R — never aborts a shared settlement path (F4).\nfunc reserveJunior(c *Court, want int64) int64 {\n\ttouch(c)\n\tr := c.reservoirR()\n\tif want \u003e r {\n\t\twant = r\n\t}\n\tif want \u003c 0 {\n\t\twant = 0\n\t}\n\tc.juniorReserved = mustAdd(c.juniorReserved, want)\n\treturn want\n}\n\n// mintEmission is the ONLY emission mint: bounds-checked against the court's\n// own ledger and counted into emittedTotal (which feeds d_eff).\nfunc mintEmission(c *Court, to address, amount int64) {\n\tif amount \u003c= 0 {\n\t\treturn\n\t}\n\tc.coin.Mint(to, amount)\n\tc.emittedTotal = mustAdd(c.emittedTotal, amount)\n}\n\n// PullSenior pays a senior entitlement as far as accrual covers it. Anyone may\n// crank it; payment always goes to the entitlement's address. Returns the\n// amount paid this call.\nfunc PullSenior(cur realm, courtSlug string, seq uint64) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tc := mustCourt(courtSlug)\n\ttouch(c)\n\tv := c.queue.Get(beClaimKey(seq))\n\tif v == nil {\n\t\tpanic(\"kourtv2: no such entitlement\")\n\t}\n\te := v.(*entitlement)\n\tcovered := c.cumAccrual - e.start\n\tif covered \u003e e.amount {\n\t\tcovered = e.amount\n\t}\n\tpay := covered - e.paid\n\tif pay \u003c= 0 {\n\t\treturn 0\n\t}\n\te.paid += pay\n\tc.seniorOwed -= pay\n\tc.seniorPaid = mustAdd(c.seniorPaid, pay)\n\tmintEmission(c, e.to, pay)\n\tchain.Emit(seniorPaidEvent,\n\t\t\"court\", c.id,\n\t\t\"seq\", strconv.FormatUint(seq, 10),\n\t\t\"to\", e.to.String(),\n\t\t\"paid\", strconv.FormatInt(pay, 10),\n\t\t\"remaining\", strconv.FormatInt(e.amount-e.paid, 10),\n\t)\n\treturn pay\n}\n\n// ------------------------------------------------------------------ reads --\n\n// Reservoir is the junior pool R; SeniorOwed is the queue's unpaid mass —\n// together the \"when do I get paid\" legibility the UX needs (§11).\nfunc Reservoir(courtSlug string) int64  { return mustCourt(courtSlug).reservoirR() }\nfunc SeniorOwed(courtSlug string) int64 { return mustCourt(courtSlug).seniorOwed }\n\n// SeniorEntitlement reads one entitlement: recipient, total, paid, and what is\n// payable right now.\nfunc SeniorEntitlement(courtSlug string, seq uint64) (to address, amount, paid, payable int64) {\n\tc := mustCourt(courtSlug)\n\tv := c.queue.Get(beClaimKey(seq))\n\tif v == nil {\n\t\tpanic(\"kourtv2: no such entitlement\")\n\t}\n\te := v.(*entitlement)\n\tcovered := c.cumAccrual - e.start\n\tif covered \u003e e.amount {\n\t\tcovered = e.amount\n\t}\n\tp := covered - e.paid\n\tif p \u003c 0 {\n\t\tp = 0\n\t}\n\treturn e.to, e.amount, e.paid, p\n}\n\n// QueueLen is the number of senior entitlements ever enqueued; seqs run\n// 1..QueueLen, so a client bounds SeniorEntitlement iteration with it (there is\n// no removal — paid entitlements stay in the queue with paid==amount).\nfunc QueueLen(courtSlug string) uint64 { return mustCourt(courtSlug).queueSeq }\n\n// Poke advances the emission clock without any other action — a public crank\n// so accrual (and thus payability) never depends on trading activity.\nfunc Poke(cur realm, courtSlug string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\ttouch(mustCourt(courtSlug))\n}\n"},{"name":"folders.gno","body":"// Folders: moderator-managed curation metadata (MODERATION.md §7). A folder is\n// a named, ordered list of claim IDs in one court. Membership gates NOTHING —\n// not answerability, not emission, not deposits, not votes (\"zero economic\n// weight\" = zero on-chain coupling). The flat newest-first claim list always\n// remains, so folders CURATE and can never BURY: an unfoldered claim is exactly\n// as reachable as before folders existed.\n//\n// Decision rule: folder ops need one ACTIVE moderator (member + not suspended),\n// not the m-of-n of consequential acts (hide/purge/suspend). A decoy or\n// mis-filing moderator has zero blast radius here — folders are zero-weight and\n// instantly reversible — so the m-of-n ceremony would only tax routine curation\n// for no security gain (the round-7 R3 rationale is about bounding damage, and\n// there is none to bound). Suspension still refuses folder writes.\n\npackage kourtv2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\nconst maxFolderTextLen = 200\n\n// NESTING (COURTS_STRUCTURE.md §6). \"A section is a heading with a slug, holding\n// claims and other sections\" — which is what a folder becomes once it can hold\n// another folder, so this is §6's V1 sections, moderator-written rather than\n// founder-written. What §6 DEFERS is *governed* sections (the heading is itself a\n// claim, affirmed by vote, its slug bound on affirmation); plain nested headings\n// are not deferred and this is them.\n//\n// It also completes §5's containment story rather than contradicting it. §5's\n// guard is \"only sections may be containment parents\": a claim's container is a\n// FOLDER (AddToFolder, already here), a folder's container is another folder\n// (this change), and claim→claim containment stays forbidden — which is why\n// association.gno implements the argument edge and not that one.\nconst (\n\t// Four, matching the depth the overlay's own curation schema has validated\n\t// since before the chain could nest at all.\n\tmaxFolderDepth = 4\n\t// One folder's claim list. FolderItems returns the WHOLE list in one read, and\n\t// AddToFolder scans it for duplicates on every add — so without a cap the read\n\t// grows with what a moderator wrote and filling a folder costs O(n²). 200\n\t// claims under one heading is already more than a heading is for.\n\tmaxFolderItems = 200\n\t// A CAP THE OVERLAY ALREADY ASSUMED AND THE CHAIN DID NOT ENFORCE. web's\n\t// CHAIN_FOLDER_CAP is 100 and it stops reading there; the realm had no limit,\n\t// so a court could hold folders the overlay would silently truncate. It is\n\t// also what makes the nesting guards affordable: the cycle walk and the\n\t// subtree-height check below are scans, and an unbounded folder count would\n\t// make a WRITE cost grow with what was already written.\n\tmaxFolders = 100\n)\n\n// folder is one curation list. name/desc are user text: sanitized at render,\n// length-capped here, and purgeable as rows (PurgeFolder). items is ordered.\ntype folder struct {\n\tid     uint64\n\tname   string\n\tdesc   string\n\titems  []uint64 // claim IDs, in curation order\n\tpurged bool     // name/desc tombstoned behind a category code\n\tcode   string\n\t// retired: struck from the tree, but the ROW SURVIVES. Not a delete, and the\n\t// difference is forced by a client: the overlay enumerates folders by walking\n\t// ids 1..FolderCount and calls that contiguity load-bearing, so removing an\n\t// entry would renumber the world underneath it. FolderCount is folders.Size(),\n\t// so a retired folder still counts and every id still resolves.\n\tretired bool\n\t/* bornOf: the claim that carried this set into existence, or 0 for one a\n\t   moderator simply created. See governedset.gno — a 𓂀 claim that settles\n\t   YES gets a set bound to it, and this is the binding. Kept on the folder\n\t   rather than in a second index because the folder is the thing that can be\n\t   retired, purged and moved, and a separate map would be a second place to\n\t   remember to do that. */\n\tbornOf uint64\n\t/* focusByDefault: this set is TICKED when a reader arrives, because the claim\n\t   that carried it in was filed with 𓂀 rather than 𓁼.\n\t   IT DOES NOT HIDE ANYTHING BY ITSELF. The filter is a spotlight: ticked sets\n\t   are what shows, and with none ticked the filter is inert and the whole\n\t   docket shows. So a court where every set is 𓁼 behaves exactly as every\n\t   court did before this existed — which is the point. A 𓂀 set is a court\n\t   saying \"start here\", and the cost of saying it is that everything outside\n\t   the 𓂀 sets waits behind a tick.\n\t   Set once at AffirmSet from the title and never again: a title cannot be\n\t   edited, so neither can this. A moderator's own CreateFolder leaves it\n\t   false — there is no mark on a folder nobody filed a claim for. */\n\tfocusByDefault bool\n\t// parent folder, 0 = a root heading. Zero on pre-nesting folders, which read\n\t// as roots — the same \"zero on pre-upgrade\" shape claimState.openedAtTime\n\t// already uses, so there is no migration step and no version field.\n\tparent uint64\n\t// Where this folder sits among its siblings, 1-based. ZERO MEANS UNPLACED,\n\t// and unplaced sorts LAST (see folderAfter) — which is the whole reason this\n\t// field costs nothing to adopt. A court that has never been ordered has every\n\t// ord at zero, so every folder is unplaced and the list falls back to id\n\t// order: exactly what it emitted before this field existed. And a folder\n\t// created AFTER an ordering lands at the end, where a new folder belongs,\n\t// instead of jumping to the front the way a plain 0-is-first rule would put\n\t// it. items has had curation order since MoveItemInFolder; this is the same\n\t// courtesy for the headings themselves.\n\tord int\n\t// ONE image, or nil (owner ruling, CLAIM_MEDIA §10). A folder is a heading,\n\t// not a claim: it is arguing nothing, so it has nothing to prove and no\n\t// numbered exhibits to keep in order. What it has is a face on the map, and\n\t// one picture is a face. Seven would be a gallery for a thing that makes no\n\t// assertion.\n\t//\n\t// Same mediaItem the chain already stores for a claim, so the fingerprint,\n\t// the mirrors, the archive-first destination and the render-time\n\t// revalidation all come along unchanged.\n\timage *mediaItem\n}\n\n// folderDepth counts headings above this one. A root is 1. Bounded by maxFolders\n// rather than by maxFolderDepth so that state which somehow holds a cycle\n// terminates here instead of hanging the transaction that found it.\nfunc (cm *courtMod) folderDepth(id uint64) int {\n\td := 0\n\tfor id != 0 \u0026\u0026 d \u003c= maxFolders {\n\t\td++\n\t\tv := cm.folders.Get(beClaimKey(id))\n\t\tif v == nil {\n\t\t\tbreak\n\t\t}\n\t\tid = v.(*folder).parent\n\t}\n\treturn d\n}\n\n// folderHeight is how deep the subtree UNDER id goes (a leaf is 1). Needed\n// because re-parenting moves a whole subtree: dropping a 2-high section under a\n// 3-deep parent would make 5, and checking only the parent's depth would miss it.\nfunc (cm *courtMod) folderHeight(id uint64) int {\n\th := 1\n\tcm.folders.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tf, ok := v.(*folder)\n\t\tif !ok || f.parent != id {\n\t\t\treturn false\n\t\t}\n\t\tif ch := 1 + cm.folderHeight(f.id); ch \u003e h {\n\t\t\th = ch\n\t\t}\n\t\treturn false\n\t})\n\treturn h\n}\n\n// mustNestable checks a proposed parent for a folder: it exists, it is not the\n// folder itself, it is not inside the folder (a cycle), and the result fits the\n// depth cap.\nfunc (cm *courtMod) mustNestable(id, parent uint64, height int) {\n\tif parent == 0 {\n\t\treturn\n\t}\n\tif parent == id {\n\t\tpanic(\"kourtv2: a folder cannot contain itself\")\n\t}\n\tcm.mustFolder(parent)\n\tfor up, n := parent, 0; up != 0 \u0026\u0026 n \u003c= maxFolders; n++ {\n\t\tif up == id {\n\t\t\tpanic(\"kourtv2: that would put a folder inside itself\")\n\t\t}\n\t\tv := cm.folders.Get(beClaimKey(up))\n\t\tif v == nil {\n\t\t\tbreak\n\t\t}\n\t\tup = v.(*folder).parent\n\t}\n\tif cm.folderDepth(parent)+height \u003e maxFolderDepth {\n\t\tpanic(\"kourtv2: folders nest at most \" + strconv.Itoa(maxFolderDepth) + \" deep\")\n\t}\n}\n\n// folderAfter reports whether a sorts after b WITHIN ONE SIBLING GROUP, on\n// (placed, ord, id).\n//\n// UNPLACED LAST is the load-bearing clause. `ord == 0` means nobody has ever\n// placed this folder, so a group nobody has ordered sorts entirely by id — and a\n// folder created after an ordering lands at the end, where a new folder belongs,\n// rather than at the head of a list the curator just arranged.\nfunc folderAfter(a, b *folder) bool {\n\tap, bp := a.ord \u003e 0, b.ord \u003e 0\n\tif ap != bp {\n\t\treturn bp // b is placed and a is not, so a goes after it\n\t}\n\tif ap \u0026\u0026 a.ord != b.ord {\n\t\treturn a.ord \u003e b.ord\n\t}\n\treturn a.id \u003e b.id\n}\n\n// folderRows is every folder in display order: the id-order spine the tree has\n// always emitted, with each sibling group PERMUTED WITHIN THE SLOTS IT ALREADY\n// OCCUPIED.\n//\n// The first version sorted globally by (parent, placed, ord, id), and grouping\n// by parent broke the promise this whole feature is built on. TestFolderTree\n// caught it: a nested court reads \"1:0:-,2:1:-,3:0:-\" and grouping re-emitted it\n// as \"1:0:-,3:0:-,2:1:-\" — every already-deployed nested court's read changing\n// shape on a feature nobody had used yet. Keeping the slots means an unordered\n// court is byte-identical, whatever its shape, which is what \"no migration\"\n// actually requires. It also keeps a parent ahead of its own children instead of\n// letting ordered children overtake it.\n//\n// An insertion sort, and deliberately not an import: nothing in this tree\n// imports \"sort\", the list is capped at maxFolders (100), and a realm's\n// dependency surface is paid for at deploy time. n² on 100 rows stays in the\n// same order of magnitude as the tree walk that produced it.\nfunc (cm *courtMod) folderRows() []*folder {\n\tout := make([]*folder, 0, maxFolders)\n\tcm.folders.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tif f, ok := v.(*folder); ok {\n\t\t\tout = append(out, f)\n\t\t}\n\t\treturn false\n\t})\n\tfor i := range out {\n\t\t// Each group once, at its first slot.\n\t\tfirst := true\n\t\tfor j := 0; j \u003c i; j++ {\n\t\t\tif out[j].parent == out[i].parent {\n\t\t\t\tfirst = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !first {\n\t\t\tcontinue\n\t\t}\n\t\tp := out[i].parent\n\t\tslots := make([]int, 0, len(out))\n\t\tgrp := make([]*folder, 0, len(out))\n\t\tfor j, g := range out {\n\t\t\tif g.parent == p {\n\t\t\t\tslots = append(slots, j)\n\t\t\t\tgrp = append(grp, g)\n\t\t\t}\n\t\t}\n\t\tfor a := 1; a \u003c len(grp); a++ {\n\t\t\tf := grp[a]\n\t\t\tb := a - 1\n\t\t\tfor b \u003e= 0 \u0026\u0026 folderAfter(grp[b], f) {\n\t\t\t\tgrp[b+1] = grp[b]\n\t\t\t\tb--\n\t\t\t}\n\t\t\tgrp[b+1] = f\n\t\t}\n\t\tfor k, g := range grp {\n\t\t\tout[slots[k]] = g\n\t\t}\n\t}\n\treturn out\n}\n\nfunc (cm *courtMod) mustFolder(id uint64) *folder {\n\tv := cm.folders.Get(beClaimKey(id))\n\tif v == nil {\n\t\tpanic(\"kourtv2: no such folder\")\n\t}\n\treturn v.(*folder)\n}\n\n// requireFolderMod: an active moderator (member, not suspended). Single-signer.\nfunc requireFolderMod(c *Court, who address) *courtMod {\n\tcm := ensureMod(c)\n\trequireActiveMod(cm, who)\n\treturn cm\n}\n\n// ---- the shared body, and who is allowed to call it ----------------------\n\n/* ONE PLACE A FOLDER COMES INTO BEING, and it checks no authority.\n   Three callers now reach it: CreateFolder and CreateFolderIn, which gate on\n   requireFolderMod, and the meta court's `mod:newset` execution, whose\n   authority is a carried verdict rather than an address. Putting the gate inside\n   would make the verdict path need a moderator to exist; leaving it out and\n   naming the rule here is the shape installModSet already uses for the same\n   reason.\n   THE CALLER CHECKS AUTHORITY. The body checks that the FOLDER is legal — the\n   cap, the text lengths, the nesting — because those are true of a folder no\n   matter who asked for it, and the verdict path has to re-check them at\n   execution anyway: weeks pass between a proposal and the verdict that carries\n   it, and a court can fill up in between. */\nfunc folderTextOK(name, desc string) bool {\n\treturn runeLen(name) \u003e 0 \u0026\u0026 runeLen(name) \u003c= maxFolderTextLen \u0026\u0026 runeLen(desc) \u003c= maxFolderTextLen\n}\n\n// folderNameTaken reports whether a LIVE folder already carries this name. A\n// retired one does not count: the row survives so ids stay contiguous, but the\n// name is free again.\nfunc folderNameTaken(cm *courtMod, name string) bool {\n\ttaken := false\n\tif cm.folders == nil {\n\t\treturn false\n\t}\n\tcm.folders.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tf := v.(*folder)\n\t\tif !f.retired \u0026\u0026 f.name == name {\n\t\t\ttaken = true\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\treturn taken\n}\n\nfunc newFolderCore(cm *courtMod, name, desc string, parent uint64) uint64 {\n\tif cm.folders.Size() \u003e= maxFolders {\n\t\tpanic(\"kourtv2: this court already has \" + strconv.Itoa(maxFolders) + \" folders\")\n\t}\n\tif !folderTextOK(name, desc) {\n\t\tpanic(\"kourtv2: a folder name is 1..200 and a description at most 200 characters\")\n\t}\n\tif parent != 0 {\n\t\t// height 1: a folder being created has no subtree yet.\n\t\tcm.mustNestable(0, parent, 1)\n\t}\n\tcm.folderSeq++\n\tid := cm.folderSeq\n\tcm.folders.Set(beClaimKey(id), \u0026folder{id: id, name: name, desc: desc, parent: parent})\n\treturn id\n}\n\n// ---- a folder the court has not agreed to yet ----------------------------\n\n/* A PROPOSAL IS NOT A FOLDER. It records what somebody wants filed and nothing\n   else: no entry in cm.folders, no id in the contiguous folder range, nothing\n   the overlay enumerates. Only a carried `mod:newset` verdict turns one into\n   a folder, and it re-checks every rule at that moment. */\ntype folderProposal struct {\n\tid     uint64\n\tname   string\n\tdesc   string\n\tparent uint64\n\tby     address\n\tat     int64\n}\n\n/* ANY HOLDER MAY PROPOSE, because proposing is the interactive half — the point\n   of the verb is that the filing system stops being the moderators' alone. It\n   creates nothing and costs gas, so an abusive proposer buys themselves an\n   unreferenced row; the appeal that would ACT on it still has to carry a bonded\n   answer and survive 72h of anyone's dispute, and the latch in meta.gno lets one\n   court host only one live folder appeal at a time.\n   VALIDATED HERE, where the caller can be told what is wrong. The alternative is\n   failing inside an execution weeks later, which nobody is watching. */\nfunc RegisterFolderProposal(cur realm, courtSlug, name, desc string, parentID uint64) uint64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\tif !folderTextOK(name, desc) {\n\t\tpanic(\"kourtv2: a folder name is 1..200 and a description at most 200 characters\")\n\t}\n\tif parentID != 0 {\n\t\tcm.mustNestable(0, parentID, 1)\n\t}\n\tif folderNameTaken(cm, name) {\n\t\tpanic(\"kourtv2: this court already has a folder by that name\")\n\t}\n\tif cm.folderProps == nil {\n\t\tcm.folderProps = bptree.NewBPTree32()\n\t}\n\tcm.folderPropSeq++\n\tid := cm.folderPropSeq\n\tcm.folderProps.Set(beClaimKey(id), \u0026folderProposal{\n\t\tid: id, name: name, desc: desc, parent: parentID, by: who, at: heightNow(),\n\t})\n\treturn id\n}\n\nfunc (cm *courtMod) mustFolderProposal(id uint64) *folderProposal {\n\tif cm.folderProps == nil {\n\t\tpanic(\"kourtv2: no such folder proposal\")\n\t}\n\tv := cm.folderProps.Get(beClaimKey(id))\n\tif v == nil {\n\t\tpanic(\"kourtv2: no such folder proposal\")\n\t}\n\treturn v.(*folderProposal)\n}\n\n// CreateFolder makes a new empty folder and returns its id.\nfunc CreateFolder(cur realm, courtSlug, name, desc string) uint64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := requireFolderMod(c, who)\n\tid := newFolderCore(cm, name, desc, 0)\n\temitModAct(c.id, 0, \"folder-create\", who)\n\treturn id\n}\n\n// CreateFolderIn makes a folder INSIDE another one. Additive rather than a wider\n// CreateFolder: that entrypoint is called by every committed scenario and txtar\n// in the tree, and a fourth argument would rewrite all of them for a value most\n// callers want to be 0 anyway.\nfunc CreateFolderIn(cur realm, courtSlug string, parentID uint64, name, desc string) uint64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := requireFolderMod(c, who)\n\tid := newFolderCore(cm, name, desc, parentID)\n\temitModAct(c.id, 0, \"folder-create\", who)\n\treturn id\n}\n\n// MoveFolder re-parents a folder, subtree and all. 0 moves it back to the root.\n// One active moderator, like every other folder op and for the stated reason:\n// zero economic weight, instantly reversible, so the m-of-n ceremony would tax\n// routine curation for no security gain.\nfunc MoveFolder(cur realm, courtSlug string, folderID, newParent uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := requireFolderMod(c, who)\n\tf := cm.mustFolder(folderID)\n\tcm.mustNestable(folderID, newParent, cm.folderHeight(folderID))\n\tf.parent = newParent\n\temitModAct(c.id, 0, \"folder-move\", who)\n}\n\n// AddToFolder appends a claim to a folder (idempotent: a claim already present\n// is not duplicated). The claim must exist; folder membership couples nothing.\nfunc AddToFolder(cur realm, courtSlug string, folderID, claimID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := requireFolderMod(c, who)\n\t_ = mustClaim(c, claimID) // the claim must exist\n\tf := cm.mustFolder(folderID)\n\tfileInto(f, claimID)\n\temitModAct(c.id, claimID, \"folder-add\", who)\n}\n\n/* THE FILING ITSELF, without the authority question. Two callers reach it — a\n   moderator through AddToFolder, and an author filing the claim they are\n   opening through OpenClaimIn — and what is true of the FOLDER is true either\n   way: already there is a no-op, and the cap is the cap. Only who may ask\n   differs, and that is settled before this is called. */\nfunc fileInto(f *folder, claimID uint64) {\n\tfor _, x := range f.items {\n\t\tif x == claimID {\n\t\t\treturn\n\t\t}\n\t}\n\tif len(f.items) \u003e= maxFolderItems {\n\t\tpanic(\"kourtv2: a folder holds at most \" + strconv.Itoa(maxFolderItems) + \" claims\")\n\t}\n\tf.items = append(f.items, claimID)\n}\n\n// RemoveFromFolder drops a claim from a folder (order-preserving).\nfunc RemoveFromFolder(cur realm, courtSlug string, folderID, claimID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := requireFolderMod(c, who)\n\tf := cm.mustFolder(folderID)\n\tout := f.items[:0]\n\tfound := false\n\tfor _, x := range f.items {\n\t\tif x == claimID {\n\t\t\tfound = true\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, x)\n\t}\n\tif !found {\n\t\tpanic(\"kourtv2: that claim is not in the folder\")\n\t}\n\tf.items = out\n\temitModAct(c.id, claimID, \"folder-remove\", who)\n}\n\n// RenameFolder edits a folder's name/description (both user text).\nfunc RenameFolder(cur realm, courtSlug string, folderID uint64, name, desc string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := requireFolderMod(c, who)\n\tif runeLen(name) == 0 || runeLen(name) \u003e maxFolderTextLen || runeLen(desc) \u003e maxFolderTextLen {\n\t\tpanic(\"kourtv2: a folder name is 1..200 and a description at most 200 characters\")\n\t}\n\tf := cm.mustFolder(folderID)\n\tif f.purged {\n\t\tpanic(\"kourtv2: this folder's text was purged and cannot be re-set\")\n\t}\n\tf.name = name\n\tf.desc = desc\n\temitModAct(c.id, 0, \"folder-rename\", who)\n}\n\n// PurgeFolder tombstones a folder's name/description behind a category code\n// (the row-level purge for a §2-inventory folder row). Global DAO, m-of-n. The\n// items list (claim IDs, no user text) is unaffected.\n// SetFolderImage gives a folder its one picture, on the same seat that renames\n// it: a folder's face is curation, and curation is the folder moderator's.\n//\n// The argument is ONE line of the same eight-field format a claim's evidence\n// uses — kind|sha256|mime|w|h|bytes|caption|mirrors — so web/media.js builds it\n// with the same code and this realm parses it with the same parser. A second\n// line is refused rather than truncated: a caller passing two has misunderstood\n// something, and taking the first silently would hide that.\n//\n// An image only. A video is a link the court cannot vouch for, which is a\n// reasonable thing to file as EVIDENCE, where the claim page says so in as many\n// words — and an unreasonable thing to make the face of a heading.\nfunc SetFolderImage(cur realm, courtSlug string, folderID uint64, media string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := requireFolderMod(c, who)\n\tf := cm.mustFolder(folderID)\n\tif f.purged {\n\t\tpanic(\"kourtv2: this folder's text was purged and cannot be re-set\")\n\t}\n\titems := parseMediaArg(media)\n\tif len(items) != 1 {\n\t\tpanic(\"kourtv2: a folder carries exactly one image\")\n\t}\n\tif items[0].kind != mediaKindImage {\n\t\tpanic(\"kourtv2: a folder's image is an image, not a video link\")\n\t}\n\tf.image = \u0026items[0]\n\temitModAct(c.id, 0, \"folder-image\", who)\n}\n\n// ClearFolderImage takes it off again. Same seat, and separate from SetFolderImage\n// because \"no picture\" reached by passing an empty string is a state somebody\n// arrives at by accident.\nfunc ClearFolderImage(cur realm, courtSlug string, folderID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := requireFolderMod(c, who)\n\tcm.mustFolder(folderID).image = nil\n\temitModAct(c.id, 0, \"folder-image-clear\", who)\n}\n\n// FolderImage answers with the folder's picture as one line of JSON, or [] — the\n// same shape ClaimMedia uses, so the overlay parses both with one function.\n//\n// A purged folder answers with nothing, exactly as a purged claim does. Nothing\n// here is a second gate: f.image is already nil after a purge, and this reads\n// what is there.\nfunc FolderImage(courtSlug string, folderID uint64) string {\n\tc := mustCourt(courtSlug)\n\tcm := c.mod\n\tif cm == nil {\n\t\treturn \"[]\"\n\t}\n\tf := cm.mustFolder(folderID)\n\tif f.image == nil {\n\t\treturn \"[]\"\n\t}\n\treturn encodeMedia([]mediaItem{*f.image})\n}\n\nfunc PurgeFolder(cur realm, courtSlug string, folderID uint64, categoryCode string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\td := ensureGlobalDAO()\n\tif !d.members.Has(who.String()) {\n\t\tpanic(\"kourtv2: only a global DAO member may purge\")\n\t}\n\tmustCategoryCode(categoryCode)\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\tf := cm.mustFolder(folderID)\n\tfire, code := approveAction(d.pending, \"purgefolder:\"+c.id+\":\"+strconv.FormatUint(folderID, 10), who, categoryCode, d.purgeM)\n\tif !fire {\n\t\treturn\n\t}\n\tf.purged = true\n\tf.code = code\n\tf.name = \"\"\n\tf.desc = \"\"\n\t// The image is the folder's text as much as its name is: a picture chosen to\n\t// stand for a heading says something, and a purge that left it would leave\n\t// the part somebody is most likely to have objected to.\n\tf.image = nil\n\temitPurge(c.id, 0, code, who)\n}\n\n// ---- reads (render; never a money path) -----------------------------------\n\n// FolderCount is how many folders a court has.\nfunc FolderCount(courtSlug string) int {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn 0\n\t}\n\treturn c.mod.folders.Size()\n}\n\n// FolderName returns a folder's name (or its tombstone code if purged).\nfunc FolderName(courtSlug string, folderID uint64) string {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\tpanic(\"kourtv2: no such folder\")\n\t}\n\tf := c.mod.mustFolder(folderID)\n\tif f.purged {\n\t\treturn \"[purged:\" + f.code + \"]\"\n\t}\n\treturn f.name\n}\n\n// FolderDesc returns a folder's description. The field has been written and\n// length-capped since folders existed and had no way out, which the overlay has\n// been carrying as \"stored but unreadable — wishlisted\". This is that.\n//\n// NO PURGE BRANCH, and its absence is the point rather than an omission. This\n// read was first written with `if f.purged { return \"\" }` and the test for it\n// passed — then failed to fail when the branch was deleted, because PurgeFolder\n// does not flag this text, it ERASES it (name and desc are both set to \"\"). The\n// branch was dead code, and dead code in a moderation path is worse than absent:\n// it reads as the thing standing between a purge and a leak when the erasure is\n// what actually stands there.\n//\n// Worth knowing, because the two differ: a purged CLAIM keeps its title and body\n// in state and every read gates on the flag, so the bytes are still on chain. A\n// purged FOLDER's text is gone. FolderName still needs its branch — it\n// substitutes the \"[purged:CODE]\" tombstone for the erased name.\nfunc FolderDesc(courtSlug string, folderID uint64) string {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\tpanic(\"kourtv2: no such folder\")\n\t}\n\treturn c.mod.mustFolder(folderID).desc\n}\n\n// FolderItems returns a copy of a folder's claim-ID list (a value slice — never\n// a pointer into realm state, borrow rule #2).\nfunc FolderItems(courtSlug string, folderID uint64) []uint64 {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\tpanic(\"kourtv2: no such folder\")\n\t}\n\tf := c.mod.mustFolder(folderID)\n\tout := make([]uint64, len(f.items))\n\tcopy(out, f.items)\n\treturn out\n}\n\n// ClaimFolders is FolderItems read backwards: which folders is this claim filed\n// in. A claim page needs it to answer the question its own heading asks — where\n// this claim sits — and until it existed the only answer was to walk every\n// folder in the court, which is FolderCount + FolderTree + a name and an item\n// list per folder. Fourteen round trips on a six-folder court, to print one\n// name. The overlay declined to pay that, so a claim on a chain court said\n// nothing at all about its folder while a curated one said \"filed under X\".\n//\n// PURGED FOLDERS ARE SKIPPED, and that is not tidiness. PurgeFolder blanks the\n// name and the picture but deliberately leaves the item list alone — retiring is\n// what un-files claims — so a purged folder still knows which claims it held.\n// Answering with it would send a reader to a tombstone with no name on it, and\n// would republish the association a purge exists to sever.\n//\n// Retired folders need no such guard: RetireFolder clears the item list, so they\n// cannot match. Said rather than assumed, because the two verbs are one word\n// apart and only one of them empties the folder.\n//\n// Order is the tree's own — folderRows, the curator's ordering — so a claim in\n// two folders lists them the way the court lists them, not the way the avl tree\n// happens to key them.\nfunc ClaimFolders(courtSlug string, claimID uint64) []uint64 {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn nil\n\t}\n\tvar out []uint64\n\tfor _, f := range c.mod.folderRows() {\n\t\tif f.purged {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, it := range f.items {\n\t\t\tif it == claimID {\n\t\t\t\tout = append(out, f.id)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn out\n}\n\n// RetireFolder strikes a folder from the tree. The gap this fills is small and\n// real: a folder created by mistake could not be got rid of at all — PurgeFolder\n// is the global DAO's m-of-n act carrying a statutory category code, for illegal\n// content, not for \"that was a typo\".\n//\n// It does NOT remove the row. See the `retired` field: the overlay walks ids\n// 1..FolderCount, so deleting an entry would renumber every folder above it.\n//\n// Refuses a folder that still has children, so the tree cannot grow orphans and\n// a moderator retires bottom-up. Clears the claim list, which un-files those\n// claims — safe by this module's founding promise, since the flat newest-first\n// docket always remains and folders \"curate and can never bury\".\nfunc RetireFolder(cur realm, courtSlug string, folderID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := requireFolderMod(c, who)\n\tf := cm.mustFolder(folderID)\n\tif cm.folderHeight(folderID) \u003e 1 {\n\t\tpanic(\"kourtv2: retire the folders inside it first\")\n\t}\n\tf.retired = true\n\tf.items = nil\n\temitModAct(c.id, 0, \"folder-retire\", who)\n}\n\n// RestoreFolder puts a retired folder back. Folder acts are \"instantly\n// reversible\" by this module's own decision rule, which is the stated reason they\n// need only one moderator rather than m-of-n — so the reverse has to exist, or\n// that rule was only ever half true.\nfunc RestoreFolder(cur realm, courtSlug string, folderID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := requireFolderMod(c, who)\n\tf := cm.mustFolder(folderID)\n\tif f.purged {\n\t\tpanic(\"kourtv2: a purged folder cannot be restored\")\n\t}\n\tf.retired = false\n\temitModAct(c.id, 0, \"folder-restore\", who)\n}\n\n// MoveItemInFolder puts a claim at a given position in a folder's list. The list\n// is documented as \"claim IDs, in curation order\" and until now that order was\n// whatever order they were added in — curation you cannot re-order is a log.\n// Bounded by maxFolderItems, like every other walk of this list.\nfunc MoveItemInFolder(cur realm, courtSlug string, folderID, claimID uint64, to int) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := requireFolderMod(c, who)\n\tf := cm.mustFolder(folderID)\n\tat := -1\n\tfor i, x := range f.items {\n\t\tif x == claimID {\n\t\t\tat = i\n\t\t\tbreak\n\t\t}\n\t}\n\tif at \u003c 0 {\n\t\tpanic(\"kourtv2: that claim is not in the folder\")\n\t}\n\tif to \u003c 0 || to \u003e= len(f.items) {\n\t\tpanic(\"kourtv2: that position is outside the folder\")\n\t}\n\tif to == at {\n\t\treturn\n\t}\n\t// Rebuild rather than slide in place: f.items is realm state, and an\n\t// in-place shuffle writes through the persisted backing array on every step,\n\t// so a rebuild is the version whose intermediate states are never stored.\n\tout := make([]uint64, 0, len(f.items))\n\tfor i, x := range f.items {\n\t\tif i == at {\n\t\t\tcontinue\n\t\t}\n\t\tif len(out) == to {\n\t\t\tout = append(out, claimID)\n\t\t}\n\t\tout = append(out, x)\n\t}\n\tif len(out) == to {\n\t\tout = append(out, claimID)\n\t}\n\tf.items = out\n\temitModAct(c.id, claimID, \"folder-order\", who)\n}\n\n// OrderFolders sets the display order of every folder under one heading.\n//\n// parentID is the heading; 0 means the root headings. ids is a comma-separated\n// list of folder ids, and the folders are placed in that order, 1..n. Sending\n// the list again with a different order is how you reorder; the call is\n// idempotent and nothing is written unless the whole list validates.\n//\n// THE LIST MUST BE EXACTLY THE SIBLING SET — every folder under that heading,\n// each exactly once, and nothing else. A subset is refused rather than accepted\n// as a partial order: unlisted folders would keep ord 0 and sort last, so\n// ordering three of five would silently demote the other two. An unknown id is\n// refused rather than ignored, so a typo cannot become a silent no-op.\n//\n// Retired and purged siblings are IN the set. Their rows survive, they still\n// appear in FolderTree behind their flags, and one left out would come back\n// wherever it happened to fall.\n//\n// An active court moderator, single-signer. Refused when the court does not\n// exist, the caller is not an active moderator, the heading does not exist, the\n// heading holds no folders, `ids` is longer than a list of folder ids could be,\n// an entry is not a number, an id is not under that heading, or an id repeats.\nfunc OrderFolders(cur realm, courtSlug string, parentID uint64, ids string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := requireFolderMod(c, who)\n\tif parentID != 0 {\n\t\tcm.mustFolder(parentID)\n\t}\n\t// BOUND THE PARSE BEFORE IT HAPPENS. strings.Split allocates one entry per\n\t// comma, so a megabyte of commas is a megabyte of work done before any check\n\t// could refuse it — the cost is paid in the argument, not in the state. 21\n\t// bytes is a uint64 at its longest plus its separator.\n\tif len(ids) \u003e maxFolders*21 {\n\t\tpanic(\"kourtv2: that is too long to be a list of folder ids\")\n\t}\n\tsibs := make([]*folder, 0, maxFolders)\n\tcm.folders.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tif f, ok := v.(*folder); ok \u0026\u0026 f.parent == parentID {\n\t\t\tsibs = append(sibs, f)\n\t\t}\n\t\treturn false\n\t})\n\tif len(sibs) == 0 {\n\t\tpanic(\"kourtv2: there are no folders under that heading to order\")\n\t}\n\tparts := strings.Split(ids, \",\")\n\tif len(parts) != len(sibs) {\n\t\tpanic(\"kourtv2: that heading holds \" + strconv.Itoa(len(sibs)) +\n\t\t\t\" folders and the order lists \" + strconv.Itoa(len(parts)) +\n\t\t\t\"; it must list every one of them, exactly once\")\n\t}\n\torder := make([]*folder, 0, len(parts))\n\tfor _, p := range parts {\n\t\tid, err := strconv.ParseUint(strings.TrimSpace(p), 10, 64)\n\t\tif err != nil {\n\t\t\tpanic(\"kourtv2: an order is a comma-separated list of folder ids\")\n\t\t}\n\t\tvar f *folder\n\t\tfor _, s := range sibs {\n\t\t\tif s.id == id {\n\t\t\t\tf = s\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif f == nil {\n\t\t\tpanic(\"kourtv2: folder \" + strconv.FormatUint(id, 10) +\n\t\t\t\t\" is not under that heading\")\n\t\t}\n\t\t// NAME THE REPEAT, because the count check above cannot see it: a list\n\t\t// that repeats one id and drops another is the right LENGTH, and without\n\t\t// this it would silently leave the dropped folder unplaced.\n\t\tfor _, x := range order {\n\t\t\tif x.id == id {\n\t\t\t\tpanic(\"kourtv2: folder \" + strconv.FormatUint(id, 10) +\n\t\t\t\t\t\" is listed twice\")\n\t\t\t}\n\t\t}\n\t\torder = append(order, f)\n\t}\n\t// Nothing above this line has written. A refusal halfway through a list would\n\t// otherwise leave half a court reordered.\n\tfor i, f := range order {\n\t\tf.ord = i + 1\n\t}\n\temitModAct(c.id, 0, \"folder-sort\", who)\n}\n\n// FolderTree is a court's whole folder structure in one read: \"id:parent:flags\"\n// per folder, comma separated, in display order.\n//\n// parent is 0 for a root heading. flags is \"r\" retired, \"p\" purged, \"i\" has a\n// picture, in that order, or \"-\" for none of them. Children follow the parent\n// they belong to, and each sibling group\n// sits in the order a curator gave it with OrderFolders; folders nobody has\n// ordered come last within their group, ids ascending. A court nobody has\n// ordered reads in plain id order.\n//\n// STRUCTURE ONLY. Names and descriptions are one read each — FolderName,\n// FolderDesc — because they are user text a client only needs for the folders it\n// actually draws. A purged folder's flags say so, so a client knows to expect a\n// tombstone rather than a name.\n//\n// Ungated: a court's filing structure is not secret. The two things that ARE\n// gated, a purged folder's name and description, are gated in the reads that\n// return them.\n//\n// Bounded at 100 folders, so the answer cannot exceed about a kilobyte whatever\n// anybody writes. Returns the empty string for a court that has no folders yet.\n// Refused only on a court that does not exist.\nfunc FolderTree(courtSlug string) string {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn \"\"\n\t}\n\tvar b strings.Builder\n\tn := 0\n\tfor _, f := range c.mod.folderRows() {\n\t\tif n \u003e 0 {\n\t\t\tb.WriteString(\",\")\n\t\t}\n\t\tflags := \"\"\n\t\tif f.retired {\n\t\t\tflags += \"r\"\n\t\t}\n\t\tif f.purged {\n\t\t\tflags += \"p\"\n\t\t}\n\t\t// \"f\" — this set is ticked on arrival. It rides the row because the filter\n\t\t// has to know before its FIRST paint; a read per folder to find out would\n\t\t// be a hundred at the cap, and the page would flicker through the wrong\n\t\t// view on the way to the right one.\n\t\tif f.focusByDefault {\n\t\t\tflags += \"f\"\n\t\t}\n\t\t// \"i\" — this folder has a picture, so a client knows to spend the read.\n\t\t// It rides here rather than in a read of its own because the alternative\n\t\t// is one FolderImage per folder on every map draw, a hundred at the cap,\n\t\t// for a field most folders will never set. The flag costs a byte and the\n\t\t// read is paid only where there is something to fetch.\n\t\tif f.image != nil {\n\t\t\tflags += \"i\"\n\t\t}\n\t\tif flags == \"\" {\n\t\t\tflags = \"-\"\n\t\t}\n\t\t// bornOf, the claim that carried this set into existence, or 0 for one a\n\t\t// moderator simply made. A FOURTH FIELD for the same reason the \"i\" flag\n\t\t// is a flag: it is a per-folder bit, and the tree row is what exists to\n\t\t// carry per-folder bits. Read separately it was one SetBornOf per folder\n\t\t// on every draw — a hundred at the cap — for a number that is almost\n\t\t// always the same size as the two beside it.\n\t\t//\n\t\t// A CLIENT THAT KNOWS ONLY THREE FIELDS IS UNHARMED. The overlay parses\n\t\t// on \":\" and takes what it recognises; the shape here only ever grows to\n\t\t// the right, exactly as it did when \"i\" joined the flags.\n\t\tb.WriteString(strconv.FormatUint(f.id, 10) + \":\" +\n\t\t\tstrconv.FormatUint(f.parent, 10) + \":\" + flags + \":\" +\n\t\t\tstrconv.FormatUint(f.bornOf, 10))\n\t\tn++\n\t}\n\treturn b.String()\n}\n\n// FolderRetired reports whether a folder has been struck from the tree. The row\n// still exists and still counts toward FolderCount; this is how a reader knows\n// to skip it.\nfunc FolderRetired(courtSlug string, folderID uint64) bool {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn false\n\t}\n\tv := c.mod.folders.Get(beClaimKey(folderID))\n\tif v == nil {\n\t\treturn false\n\t}\n\treturn v.(*folder).retired\n}\n\n// FolderParent is the folder this one sits in, 0 for a root heading. The overlay\n// assembles the tree from FolderCount + this, one cheap read each, rather than\n// the chain returning a nested structure — no read here grows with the shape of\n// what somebody built.\nfunc FolderParent(courtSlug string, folderID uint64) uint64 {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn 0\n\t}\n\tv := c.mod.folders.Get(beClaimKey(folderID))\n\tif v == nil {\n\t\treturn 0\n\t}\n\treturn v.(*folder).parent\n}\n\n// FolderPurged reports whether a folder's text was tombstoned.\n//\n// The nil branch PANICS, matching FolderName and FolderItems above and the\n// mustFolder they share. It used to return false, which made the same\n// nonexistent folder answer two different ways depending on whether the court\n// happened to hold any moderation state at all: false on a court with no mod\n// object, \"no such folder\" on one that had ever been moderated. A caller\n// probing an id it had not validated would see the discrepancy only on the\n// second kind of court. The v0.19 rule again — a lazily-created read's\n// not-yet-materialised branch must answer what the materialised one would.\nfunc FolderPurged(courtSlug string, folderID uint64) bool {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\tpanic(\"kourtv2: no such folder\")\n\t}\n\treturn c.mod.mustFolder(folderID).purged\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/kourt/kourtv2\"\ngno = \"0.9\"\n"},{"name":"governedset.gno","body":"package kourtv2\n\n/* GOVERNED SETS — §6's deferred half, un-deferred.\n *\n * A claim whose title begins with the wedjat, U+13080 𓂀, is asking to become a\n * set. It is a claim in every other respect: it is staked on, answered under\n * bond, disputable for 72 hours, and it settles YES or NO like any other. What\n * the mark buys is what happens AFTER a YES — the court has agreed that this\n * heading should exist, so a set is created and bound to the claim that carried\n * it.\n *\n * THE CLAIM DOES NOT BECOME THE CONTAINER, and that is not a compromise, it is\n * what §6 actually specified: \"the heading is itself a claim, affirmed by vote,\n * its slug bound on affirmation\". §5's guard — \"only sections may be containment\n * parents ... claim→claim containment stays forbidden\" — is why association.gno\n * implements the argument edge and not that one, and it still holds here: the\n * container is a folder, exactly as it was before, and the claim owns it.\n * Reading it as \"a set IS a claim\" is right at the level a reader cares about\n * and wrong at the level the storage cares about, and only one of those has to\n * be true for the feature to be worth having.\n *\n * WHY THIS IS NOT mod:newset AGAIN. That verb asks the META court to overrule\n * a court's moderators, and its proposal carries the name because a claim title\n * could not. This asks the court ITSELF, in its own venue, and the title IS the\n * name — which is the whole appeal of the mark. The two do not overlap: one is\n * an appeal against moderators who will not file, the other is a court filing\n * for itself. A court with no moderators at all can use this and cannot use\n * that.\n *\n * EXECUTION IS A SEPARATE TRANSACTION, following ExecuteMetaVerdict rather than\n * inventing a second convention: settling a claim must not do unbounded work,\n * and every rule has to be re-checked at the moment of execution anyway, because\n * weeks pass between opening a claim and carrying it and a court can fill up or\n * take the name in between.\n */\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"chain\"\n)\n\n/* THE MARK IS ONE EXACT CODEPOINT, U+13080 EGYPTIAN HIEROGLYPH D010.\n *\n * SPELLED AS AN ESCAPE, NOT AS THE GLYPH, so that what this constant is cannot\n * depend on an editor, a terminal font or a copy-paste. The Egyptian block holds\n * several eyes that are indistinguishable at title size — D004 𓁹, D006, D010,\n * D010 𓂀 — and a source file that shows the reader a picture instead of a\n * number is a file where the wrong one can be pasted in and nobody will see it.\n *\n * AND THE MATCH IS EXACT. parseModTitle's comment makes the same promise for\n * \"mod:\" — \"a near-miss cannot masquerade as binding\" — and the promise is worth\n * more here, because the near-misses are invisible rather than merely similar.\n * A claim opening with D004 is an ordinary claim with an unusual first\n * character; it is NOT a set, and it must not become one by looking like it.\n */\nconst (\n\t/* TWO MARKS, AND THE CHOICE BETWEEN THEM IS THE SET'S DEFAULT.\n\t *\n\t * 𓂀 D010, the wedjat, opens SHOWN. 𓁼 D007, the eye with painted lids, opens\n\t * CONCEALED. Both make a set; the one an author typed says which state the set\n\t * is in when a reader arrives, and nothing else distinguishes them.\n\t *\n\t * \"CONCEALED\", NOT \"HIDDEN\", AND THE WORD IS DELIBERATE. This realm already\n\t * spends \"hidden\" on a different thing: HideItem and HiddenFromListing are a\n\t * MODERATOR taking a claim out of ordinary discovery, an act with an authority\n\t * and a log row behind it. A set that opens concealed has had nothing done to\n\t * it — the author chose which mark to type, and a reader unticks a filter to\n\t * see it. Two states a page must never conflate, so they do not share a word.\n\t * The overlay says \"set that opens shown\" / \"set that opens concealed\" for the\n\t * same reason.\n\t *\n\t * THE MARK WAS ALREADY GOING TO BE THERE, which is the whole reason to spend\n\t * it this way: no second field, no second read, no second thing to keep true.\n\t * A title is immutable, so the default is immutable too — a set opens the way\n\t * it was filed, and changing it means filing a different set.\n\t *\n\t * D007 IS NOT \"CLOSED\" IN UNICODE. It is the eye with cosmetic lines along\n\t * the lids, and no codepoint means \"not seeing\". This realm uses it that way\n\t * by convention, which is a decision and is written here so it reads as one.\n\t */\n\tsetMark   = \"\\U00013080\" // 𓂀 U+13080 — a set that opens shown\n\tshutMark  = \"\\U0001307C\" // 𓁼 U+1307C — a set that opens concealed\n\tsetPrefix = setMark + \" \"\n\tshutPrefix = shutMark + \" \"\n)\n\n/* parseSetTitle reads a governed-set title. The mark, one space, then the name.\n *\n * THE SPACE IS REQUIRED, so that the mark is a prefix and not merely a first\n * character: without it \"𓂀Lab leak\" and \"𓂀 Lab leak\" would be two spellings of\n * one thing, and a court would eventually hold both.\n *\n * THE NAME IS THE REST, VERBATIM — no trimming beyond the single delimiter\n * space. A title with trailing whitespace is a different title, and folderTextOK\n * is what decides whether the result is a legal name; silently repairing the\n * caller's string here would mean the set's name and the claim's title no longer\n * match, which is the one invariant this feature sells.\n */\nfunc parseSetTitle(title string) (name string, shown bool, ok bool) {\n\tpre := \"\"\n\tswitch {\n\tcase strings.HasPrefix(title, setPrefix):\n\t\tpre, shown = setPrefix, true\n\tcase strings.HasPrefix(title, shutPrefix):\n\t\tpre, shown = shutPrefix, false\n\tdefault:\n\t\treturn \"\", false, false\n\t}\n\tname = title[len(pre):]\n\tif !folderTextOK(name, \"\") {\n\t\treturn \"\", false, false\n\t}\n\treturn name, shown, true\n}\n\n// IsSetClaim reports whether this claim's title asks to be a set. It says\n// nothing about whether the court agreed — see ClaimSet for that.\nfunc IsSetClaim(courtSlug string, claimID uint64) bool {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\t_, _, ok := parseSetTitle(cs.title)\n\treturn ok\n}\n\n/* ClaimSet is the binding, read forwards: which set did this claim create, or 0.\n *\n * A SCAN, AND DELIBERATELY SO. The reverse direction is a field on the folder,\n * so this walks folders looking for it. That is bounded by maxFolders, which is\n * the same bound folderNameTaken already walks on every single AddToFolder, so\n * this is not a new cost class — and a second index would be a second thing to\n * keep true.\n *\n * AND IT COUNTS RETIRED ROWS, which is where it parts company with\n * folderNameTaken beside it — that one skips them on purpose, so a retired\n * folder's NAME comes free again. The asymmetry is not an oversight: a name is a\n * label and may be reused, a birth is an event and happened once. Make this skip\n * retired rows to match, and New's exactly-once guard reads zero again on a\n * claim whose set was retired — so it mints a second folder, and a moderator\n * retiring each one in turn pumps a single settled claim against maxFolders, one\n * carry per retirement, with RestoreFolder to put them all back afterwards.\n * Pinned by TestRetiringABornSetDoesNotFreeTheClaim.\n */\nfunc ClaimSet(courtSlug string, claimID uint64) uint64 {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil || c.mod.folders == nil {\n\t\treturn 0\n\t}\n\tvar out uint64\n\tc.mod.folders.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tf := v.(*folder)\n\t\tif f.bornOf == claimID {\n\t\t\tout = f.id\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\treturn out\n}\n\n// SetBornOf is the binding read backwards: which claim carried this set into\n// existence, or 0 for one a moderator simply created.\nfunc SetBornOf(courtSlug string, folderID uint64) uint64 {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn 0\n\t}\n\treturn c.mod.mustFolder(folderID).bornOf\n}\n\n/* New carries a settled 𓂀 claim into the set it asked to be.\n *\n * THE VERB IS \"NEW\" — you new a set. It was AffirmSet, which named the mechanism\n * (a verdict being affirmed) rather than the act, and read as bookkeeping for\n * something a court decided. The button has said \"New\" since the door was built;\n * the entrypoint, the act code and the word a person would use are now one thing.\n *\n * GENERIC ON PURPOSE, AND SAFE HERE. `New` is a bare name in a package with 172\n * exported reads and no other `New` — a realm's exported surface is addressed as\n * kourtv2.New, so the qualifier is always present at the call site, and MsgCall\n * names it in full. What it buys is that the chain, the page and the sentence\n * agree, which is the whole argument for a governed set having a verb at all.\n *\n * PERMISSIONLESS, like ExecuteMetaVerdict: the verdict is the authority, and\n * making execution a privilege would let whoever holds it sit on a decision the\n * court already took.\n *\n * NO DESCRIPTION. A moderator's folder has a one-line desc because nothing else\n * says what belongs in it; this set has a whole claim — with a body, a stake\n * history and a verdict — sitting behind it, and that is a better description\n * than a line anybody could have written. Leaving it empty also means there is\n * exactly one place the text lives, so the two can never disagree.\n *\n * AT THE ROOT. Nesting a governed set under another would need the parent named\n * somewhere, and the title is spent on the name. That is a real limit and it is\n * stated rather than hidden: a moderator can MoveFolder it afterwards, which is\n * the same authority that could have retired it anyway.\n */\nfunc New(cur realm, courtSlug string, claimID uint64) uint64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\n\t// 1. The title still has to parse. It cannot change under a claim, but\n\t//    reading it here rather than trusting a flag set at open means there is\n\t//    one definition of what a set title is, in one function.\n\tname, shown, ok := parseSetTitle(cs.title)\n\tif !ok {\n\t\tpanic(\"kourtv2: this claim's title is not a set heading\")\n\t}\n\n\t// 2. A FINAL YES. provClose is the three-failed-rounds close, which is not a\n\t//    verdict at all — meta.gno refuses on exactly the same pair, and for the\n\t//    same reason: `provisional` alone would read a closed-without-decision\n\t//    claim as a YES.\n\tif cs.verdictAt == 0 || cs.provClose {\n\t\tpanic(\"kourtv2: no final verdict\")\n\t}\n\tif cs.provisional != 0 {\n\t\tpanic(\"kourtv2: the verdict was not YES\")\n\t}\n\n\t// 3. Exactly once. Without this anyone could re-call a settled claim and\n\t//    mint a set per call until the court hit its folder cap.\n\tif prior := ClaimSet(courtSlug, claimID); prior != 0 {\n\t\tpanic(\"kourtv2: this claim already carries set \" + strconv.FormatUint(prior, 10))\n\t}\n\n\t// 4. RE-CHECKED NOW, not at open. Weeks pass between opening a claim and\n\t//    carrying it: an ordinary CreateFolder can take the name in between, and\n\t//    the court can fill up. A name that has since been taken is a REFUSAL —\n\t//    the same call mod:newset makes, and for the same reason: two live\n\t//    folders sharing a name is worse than a claim that has to be re-filed.\n\tcm := ensureMod(c)\n\tif folderNameTaken(cm, name) {\n\t\tpanic(\"kourtv2: a folder by that name already exists\")\n\t}\n\n\t/* 5. THE PARENT IS WHERE THE CLAIM ITSELF IS FILED, which is what makes a\n\t   SUBSET expressible at all. The title is spent on the name and carries no\n\t   room for a path, so the alternative was to birth every set at the root and\n\t   have a moderator move it — which put a moderator back in the middle of the\n\t   one act this feature exists to take out of their hands.\n\t   Filed in one set, born inside it. Filed in none, born at the root.\n\t   AND FILED IN SEVERAL IS A REFUSAL, not a guess. A claim may belong to any\n\t   number of sets — ClaimFolders returns a SLICE precisely because of that — so\n\t   \"the parent\" is only well defined when there is exactly one. Picking the\n\t   first would make the tree depend on iteration order, which is the kind of\n\t   thing that is stable until the day it is not.\n\t   (That sentence said \"ClaimSets\", which is not a function in this realm and\n\t   never has been — a wrong name in the one passage whose point is that two\n\t   reads sit a letter apart and confusing them nests every set under itself.)\n\t   newFolderCore re-checks the depth cap for us: a parent already four deep\n\t   refuses there, with the message that names the cap.\n\t   TWO READS, ONE LETTER APART, and they answer opposite questions:\n\t     ClaimFolders(claim) — the sets this claim is FILED IN  (the parent)\n\t     ClaimSet(claim)     — the set this claim BORE          (guard 3, above)\n\t   Reading the wrong one here would silently nest every set under itself. */\n\tvar parent uint64\n\tif in := ClaimFolders(courtSlug, claimID); len(in) == 1 {\n\t\tparent = in[0]\n\t} else if len(in) \u003e 1 {\n\t\tpanic(\"kourtv2: this claim is filed in more than one set; a born set needs exactly one parent\")\n\t}\n\n\t/* 6. AND THE PARENT HAS TO STILL BE IN THE TREE. AddToFolder has no retired\n\t   guard and mustFolder hands back a retired row like any other, so a claim can\n\t   be filed INTO a folder that has been struck — and ClaimFolders reports it,\n\t   because that read skips purged rows and only purged rows. Confirmed on a\n\t   running chain, not inferred.\n\t   Without this, step 5 picks that folder and newFolderCore builds under it:\n\t   mustNestable checks depth and cycles, never liveness. The result is a LIVE\n\t   folder hanging off a retired one — exactly the shape RetireFolder refuses to\n\t   create, reached from underneath instead of from above. Clients survive it\n\t   (nestChainFolders roots a folder whose parent was skipped) but a client\n\t   being defensive is not the chain being right.\n\t   A REFUSAL, NOT A QUIET RE-ROOT, on the same argument step 5 makes about\n\t   several filings: a guess is the wrong answer when the caller can say. One\n\t   RestoreFolder or one RemoveFromFolder makes it decidable, and the claim\n\t   keeps everything the court gave it in the meantime — nothing here expires.\n\t   HERE RATHER THAN AT OPEN, with the name check above it, and for that guard's\n\t   own reason: weeks pass between filing a claim and carrying it, so what was\n\t   true when it was filed has to be true again now. */\n\tif parent != 0 \u0026\u0026 cm.mustFolder(parent).retired {\n\t\tpanic(\"kourtv2: the set this claim is filed in has been retired\")\n\t}\n\n\tfid := newFolderCore(cm, name, \"\", parent)\n\tf := cm.mustFolder(fid)\n\tf.bornOf = claimID\n\t// The mark the author filed under, carried onto the set it became.\n\tf.focusByDefault = shown\n\t/* ITS OWN ACT CODE, BECAUSE THE ADDRESS BESIDE IT MEANS SOMETHING ELSE HERE.\n\t *\n\t * `by` IS THE AUTHOR, NOT THE SENDER, and that is deliberate: carrying is\n\t * permissionless and mechanical — once the verdict lands, anybody presses it and\n\t * it was going to happen — so who happened to press it is noise, while the\n\t * address that framed the heading and put it to the court is the one worth\n\t * recording. Every OTHER emitModAct call in this realm passes the sender, which\n\t * is what makes the pair ambiguous rather than the address wrong.\n\t *\n\t * SO THE CODE HAD TO STOP BEING `folder-create`. Sharing it with CreateFolder\n\t * meant a consumer of the event stream could not tell a moderator exercising\n\t * discretion from a court decision being executed — and would count this row as\n\t * moderation activity by an address that took no moderator action at all. One\n\t * string separates them, and now `by` is unambiguous in both: on folder-create\n\t * it is who decided, on new it is who asked.\n\t *\n\t * AND THE CODE IS THE VERB, not a third spelling of it. It was `set-affirm`\n\t * while the entrypoint was AffirmSet; both are `new` now, so a reader who sees\n\t * the row can find the function without a lookup table.\n\t *\n\t * NOT OBSERVABLE BY THIS SUITE, and that is structural rather than an omission.\n\t * Only a filetest's `Events:` directive can see emitted events, filetests run\n\t * from OUTSIDE the package, and reaching a settled claim from outside needs court\n\t * coin — which needs Buy, which refuses a filetest outright because `main(cur\n\t * realm)` runs in an ephemeral realm and Buy demands IsUserCall. z_events_filetest\n\t * holds the folder verbs precisely because those need no coin. The gap is named\n\t * here rather than left for someone to discover by changing this string and\n\t * seeing the suite stay green. */\n\temitModAct(c.id, claimID, \"new\", cs.author)\n\tchain.Emit(\"SetAffirmed\",\n\t\t\"court\", c.id,\n\t\t\"claim\", strconv.FormatUint(claimID, 10),\n\t\t\"folder\", strconv.FormatUint(fid, 10),\n\t\t\"parent\", strconv.FormatUint(parent, 10),\n\t\t\"name\", name,\n\t\t\"height\", eventHeight(),\n\t)\n\treturn fid\n}\n"},{"name":"help.gno","body":"// The page that explains the thing, for somebody who has never seen it.\n//\n// WHY IT IS A REALM PAGE AND NOT A WEBSITE. Read the front page as a stranger:\n// a one-line lede, then a list of court names and coin symbols. Nothing says\n// what a court is, what a claim is, what staking does, or why the strangest and\n// most important property here — that you cannot lose your stake — is true.\n// Every one of those answers lived in docs/ or in the overlay, which is to say\n// nowhere a gnoweb reader could reach. A realm whose explanation is off-chain is\n// a realm you have to be told about before you can use it.\n//\n// THE ROUTE CANNOT COLLIDE WITH A COURT, structurally rather than by\n// reservation. A slug is 1..11 characters (mustSlug), and \"how-it-works\" is 12,\n// so no court can ever be registered at this path and no reserved-word list has\n// to be maintained to keep it that way. reservedSlugs exists for symbols that\n// could impersonate an asset in a wallet; a route that is simply too long to be\n// a slug needs no entry there and cannot be shadowed by an older court that\n// registered the name first.\n\npackage kourtv2\n\nimport \"strings\"\n\n// helpPath is the one place the route is spelled, so the carve in render() and\n// every link to it cannot drift apart.\nconst helpPath = \"how-it-works\"\n\n// helpLink is the phrase the rest of the realm points at this page with. One\n// wording everywhere: a reader who has already dismissed it once should be able\n// to recognise it and skip it, which they cannot do if every page invents its\n// own words for the same page.\nconst helpLink = \"[New here? How Kourt works](/r/kourt/kourtv2:\" + helpPath + \")\"\n\n// renderHelp answers, in order, the questions a stranger actually has: what is\n// this, what happens to my money, what is a court, what is a claim, where is the\n// argument, what does it cost, who is in charge.\n//\n// PLAIN WORDS, AND SHORT. The realm's own vocabulary — principal, standing,\n// epoch, reservoir, emission — is precise and means nothing to somebody who has\n// not been told what it means. Each term is either explained here on first use\n// or not used at all. A page that needs the glossary it was written to replace\n// is not a help page.\nfunc renderHelp() string {\n\tvar b strings.Builder\n\tb.WriteString(\"# How Kourt works\\n\\n\")\n\tb.WriteString(\"Kourt is a place to put money behind whether a factual claim is \" +\n\t\t\"true — without being able to lose the money.\\n\\n\")\n\t// THE ORDER OF OPERATIONS, before anything else, because every sentence\n\t// below depends on knowing which of the two currencies is in play. An\n\t// earlier draft of this page said \"stake GNOT\", which is wrong —\n\t// stake.gno's Stake, \"Stake backs one side of a claim with CC\" — and it is the\n\t// worst possible sentence to get wrong, since a reader who believes they are\n\t// staking GNOT has the whole model upside down before they reach paragraph\n\t// two.\n\tb.WriteString(\"It works in two steps. **GNOT buys a court's coin**, one way; \" +\n\t\t\"**that coin is what you stake** on the court's claims.\\n\\n\")\n\n\t// THE 1× GUARANTEE FIRST, because it is the load-bearing surprise. Every\n\t// reader arrives with a mental model built from prediction markets, where\n\t// the losing side funds the winning one. Leave that model in place and\n\t// nothing below reads correctly: \"stake NO\" sounds like a bet they can lose.\n\tb.WriteString(\"## You cannot lose your stake\\n\\n\")\n\tb.WriteString(\"Stake the court's coin on YES or NO and the stake stays yours. \" +\n\t\t\"It is locked where it is, not handed to anybody: when the claim is \" +\n\t\t\"answered you withdraw exactly what you put in — 1×, always, whichever \" +\n\t\t\"side you took and whichever side turned out to be right. No one's stake \" +\n\t\t\"pays anyone else's winnings.\\n\\n\")\n\t// TWO DIFFERENT THINGS BEING RIGHT EARNS, and the page used to run them\n\t// together as \"the court's coin … is what gives you a voice\". Coin is a\n\t// balance and standing is a score; the pass is the only place they touch,\n\t// and a reader who thinks a big balance buys a voice has been told the one\n\t// thing the design is built to prevent.\n\tb.WriteString(\"Then what is at stake? Being right pays you more of the court's \" +\n\t\t\"coin, out of the reserve it set aside for exactly that — and being wrong \" +\n\t\t\"pays you none. Nothing of yours is taken either way.\\n\\n\")\n\tb.WriteString(\"Being right also earns **standing**, which is a separate thing \" +\n\t\t\"from a balance: a score you cannot buy, and what actually gives you a \" +\n\t\t\"voice here.\\n\\n\")\n\n\tb.WriteString(\"## A court is a subject\\n\\n\")\n\tb.WriteString(\"A court covers one subject — an epidemic, an election, a company. \" +\n\t\t\"Each court has its own coin, and coins never cross between courts. Being \" +\n\t\t\"right about virology earns you nothing in a court about monetary policy. \" +\n\t\t\"That is the point: standing is earned subject by subject, by the people who \" +\n\t\t\"follow that subject.\\n\\n\")\n\n\tb.WriteString(\"## A claim is a statement that can be settled\\n\\n\")\n\tb.WriteString(\"A claim is a sentence that is either true or false, written so it \" +\n\t\t\"can actually be answered. You stake YES or NO, and you can move your stake \" +\n\t\t\"freely until an answer posts.\\n\\n\")\n\t// Verbatim as claimStatus prints them, so a reader who saw a word on a court\n\t// page finds the same word here rather than a synonym they have to guess is\n\t// the same thing.\n\t//\n\t// NOT \"three ways\", and not a list of ENDINGS. It said both, and was wrong\n\t// twice: \"disputed\" is a state a claim passes THROUGH — claimStatus reaches\n\t// it mid-life and it resolves — while \"closed without a decision\", which is\n\t// a real ending, was missing entirely. A reader landing on that status found\n\t// a state this page said could not exist.\n\tb.WriteString(\"A claim's page always says where it stands. The wordings you will see:\\n\\n\")\n\tb.WriteString(\"- **settled YES** or **settled NO** — answered, and the page \" +\n\t\t\"says which way; every stake withdraws 1×\\n\")\n\tb.WriteString(\"- **disputed** — a sealed vote is deciding; principal is never withheld\\n\")\n\tb.WriteString(\"- **closed** — never answered; every stake exited 1×, the deposit refunded\\n\")\n\tb.WriteString(\"- **closed without a decision** — three dispute rounds failed quorum; \" +\n\t\t\"everyone withdraws 1×, deposit and fee refunded\\n\\n\")\n\n\tb.WriteString(\"## Comments are where the argument is\\n\\n\")\n\tb.WriteString(\"Every claim has a comment board. A YES/NO number cannot carry a \" +\n\t\t\"reason, a source, or an objection; the board is where those go.\\n\\n\")\n\tb.WriteString(\"Writing takes standing — the score you earned by being right in \" +\n\t\t\"this court, not the coin you hold. If you have none yet, you can buy a \" +\n\t\t\"one-off entry pass by burning a little of the court's coin, which is the \" +\n\t\t\"single place money substitutes for it. Standing sets how many comments a \" +\n\t\t\"day you get, not how loudly they land: nothing is hidden for being new, \" +\n\t\t\"and nothing is ranked by wealth.\\n\\n\")\n\t// THE BADGE, EXPLAINED WHERE IT IS NOT SHOWN. It appears on the board with\n\t// no gloss, and \"YES 12000000\" beside a stranger's name is either a\n\t// disclosure or a scoreboard depending on what the reader assumes it is.\n\tb.WriteString(\"If you have money on the claim you are commenting on, your comment \" +\n\t\t\"says so — which side, and how much. The board deliberately does *not* order \" +\n\t\t\"by it. You are told, and you decide what it is worth.\\n\\n\")\n\n\tb.WriteString(\"## Two things you can actually lose\\n\\n\")\n\tb.WriteString(\"Everything above is about a stake that always comes back. \" +\n\t\t\"There are exactly two actions here that risk money, and both are \" +\n\t\t\"challenges — you are putting up a **bond** to say the court got \" +\n\t\t\"something wrong.\\n\\n\")\n\tb.WriteString(\"- **Challenging an answer.** If you think a claim was answered \" +\n\t\t\"the wrong way, you can dispute it by posting a bond. Win and the \" +\n\t\t\"answerer's bond is burned and you are compensated; lose and yours is \" +\n\t\t\"burned instead. Nothing passes between you — a forfeit is destroyed, a \" +\n\t\t\"compensation is newly issued.\\n\")\n\tb.WriteString(\"- **Challenging a rating.** Every answered claim gets rated \" +\n\t\t\"low, mid or high, and that rating decides what it pays. Anyone may \" +\n\t\t\"challenge the rating once, also by posting a bond, which calls a vote \" +\n\t\t\"on it.\\n\\n\")\n\tb.WriteString(\"**Your stake is never at risk in either.** A bond is separate \" +\n\t\t\"money you choose to put up; the coin you staked on the claim still \" +\n\t\t\"comes back 1×, whatever the challenge decides.\\n\\n\")\n\tb.WriteString(\"## What things cost\\n\\n\")\n\tb.WriteString(\"- **Staking** costs only the stake, which you get back.\\n\")\n\tb.WriteString(\"- **Opening a claim** takes a deposit, refunded when the claim is \" +\n\t\t\"answered or closed.\\n\")\n\tb.WriteString(\"- **Buying a court's coin** burns GNOT one way. There is no sell \" +\n\t\t\"side and no redemption; the price only rises as more is bought.\\n\\n\")\n\n\tb.WriteString(\"## Who is in charge\\n\\n\")\n\tb.WriteString(\"Each court elects its own moderators. They answer claims, organise \" +\n\t\t\"them into folders, and can hide comments. What they do to a claim or a \" +\n\t\t\"comment is written to that court's moderation log, which anyone can read \" +\n\t\t\"without asking. The serious powers need several moderators to agree — one \" +\n\t\t\"moderator acting alone cannot use them.\\n\\n\")\n\n\tb.WriteString(\"[← all courts](/r/kourt/kourtv2:)\\n\")\n\treturn b.String()\n}\n"},{"name":"holders.gno","body":"package kourtv2\n\n// The holder leaderboard: who holds a court's coin, largest first.\n//\n// WHAT IT RANKS, AND WHAT IT LEAVES OUT. This is COIN HELD — the ledger balance,\n// which is the coin sitting in a holder's own account. Coin that has been staked\n// on a claim is not in that balance: staking moves it to the court's custody\n// until the claim resolves. So the most committed holder in a court can rank\n// below somebody who has never staked anything, and the caller has to say so.\n// TopHoldersNote carries that sentence rather than leaving every surface to\n// invent its own.\n//\n// Ranking by held-plus-staked would be the truer figure and is not offered,\n// because it is not cheap: the stake index is keyed by address, so totalling a\n// holder's stake means walking their positions, and doing that for every holder\n// turns one read into a walk of the whole court. A leaderboard that costs the\n// node a full traversal is a denial-of-service surface with a friendly name.\n//\n// NO NEW STATE. The ledger's accounts tree already holds every balance; this\n// reads it through WalkAccounts. The alternative — a ranked index maintained at\n// every mint, burn and transfer — would be a second copy of a fact the ledger\n// already stores, and a second copy is a thing that can disagree.\n//\n// THE COURT'S OWN ESCROW IS NOT A HOLDER. It carries deposits and bonds held on\n// everyone's behalf, so leaving it in would put the court itself at the top of\n// its own leaderboard, above every person in it.\n\n// topHoldersMax bounds one page. The walk is over every account, so the cap\n// bounds the RESPONSE rather than the work — the work is the court's holder\n// count either way, which is why this is not a paging API: an offset would cost\n// the same full walk and invite somebody to page through it repeatedly.\nconst topHoldersMax = 50\n\n// HolderCount is how many addresses hold any of this court's coin — the\n// denominator a rank is out of. Zero balances are not counted: an account can\n// sit in the ledger at zero because the balance path never removes it (removal\n// would drop the delegate and the vote history with it), and \"holders: 40\" when\n// 12 of them hold nothing is a wrong number, not a subtle one.\nfunc HolderCount(courtSlug string) int {\n\tc := mustCourt(courtSlug)\n\tesc := c.escrow\n\tn := 0\n\tc.coin.WalkAccounts(func(who address, bal int64) bool {\n\t\tif bal \u003e 0 \u0026\u0026 who != esc {\n\t\t\tn++\n\t\t}\n\t\treturn false\n\t})\n\treturn n\n}\n\n// TopHolders is the leaderboard, largest balance first, as rows of\n//\n//\taddress:balance\n//\n// separated by \";\". Balance is in the coin's smallest unit, the same figure\n// CoinBalanceOf returns. limit is capped at topHoldersMax; zero or less means\n// that cap. Returns the empty string for a court with no holders.\n//\n// TIES ARE BROKEN BY ADDRESS, ascending, because WalkAccounts walks in address\n// order and the insertion below is stable. That matters for a leaderboard: two\n// holders with equal balances must not swap places between two reads of the\n// same state, or the page appears to move while nothing happened.\nfunc TopHolders(courtSlug string, limit int) string {\n\tif limit \u003c= 0 || limit \u003e topHoldersMax {\n\t\tlimit = topHoldersMax\n\t}\n\tc := mustCourt(courtSlug)\n\tesc := c.escrow\n\n\t// A fixed-size insertion sort over the top `limit`. The alternative is\n\t// collecting every holder and sorting, which allocates the whole court to\n\t// return ten rows.\n\twho := make([]address, 0, limit)\n\tbal := make([]int64, 0, limit)\n\tc.coin.WalkAccounts(func(a address, b int64) bool {\n\t\tif b \u003c= 0 || a == esc {\n\t\t\treturn false\n\t\t}\n\t\t// STRICTLY GREATER, so an equal balance does not displace the holder\n\t\t// already there — the walk is address-ascending, so the earlier address\n\t\t// keeps the higher rank and the order is total.\n\t\tpos := len(who)\n\t\tfor pos \u003e 0 \u0026\u0026 b \u003e bal[pos-1] {\n\t\t\tpos--\n\t\t}\n\t\tif pos \u003e= limit {\n\t\t\treturn false\n\t\t}\n\t\tif len(who) \u003c limit {\n\t\t\twho = append(who, address(\"\"))\n\t\t\tbal = append(bal, 0)\n\t\t}\n\t\tfor i := len(who) - 1; i \u003e pos; i-- {\n\t\t\twho[i], bal[i] = who[i-1], bal[i-1]\n\t\t}\n\t\twho[pos], bal[pos] = a, b\n\t\treturn false\n\t})\n\n\tout := \"\"\n\tfor i := range who {\n\t\tif bal[i] \u003c= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tout += \";\" + string(who[i]) + \":\" + itoa(bal[i])\n\t}\n\tif out == \"\" {\n\t\treturn \"\"\n\t}\n\treturn out[1:]\n}\n\n// TopHoldersNote is the sentence a surface showing TopHolders has to print, kept\n// here so the realm and every client say the same thing about the same figure.\n// See the file header: staked coin is in the court's custody, not the holder's\n// balance, so this ranks what is held and not what is committed.\nfunc TopHoldersNote() string {\n\treturn \"Ranked by coin held. Coin staked on a claim sits in the court's custody until the claim resolves, so it is not counted here.\"\n}\n"},{"name":"lock.gno","body":"package kourtv2\n\n// Staked CC is LOCKED IN PLACE, not taken into custody.\n//\n// Stake used to be `Transfer(who, escrow, amount)`. That bought nothing: stake\n// principal is never slashed and never burned, and WithdrawStake returns it 1×\n// whichever way the verdict went. What it cost was the staker's vote — and not\n// on the claim they staked, which would be defensible, but COURT-WIDE, on every\n// election and every quality poll in that court for as long as their coins sat\n// in the escrow. The design only ever wanted the claim-scoped exclusion, and it\n// already has one that works by RULE rather than by custody (isParticipant).\n//\n// So the coins stay in the holder's own balance and this file records a claim on\n// them instead. The deposit and the five bonds are NOT locks and are still\n// transferred: the realm has to be able to burn or redirect those on a loss, so\n// it must actually hold them.\n//\n// WHAT CUSTODY WAS ENFORCING FOR FREE, AND WHY THIS FILE IS THE RISK\n//\n// Custody made double-committing arithmetically impossible. The coins physically\n// left the balance, so the LEDGER refused the second spend, automatically and on\n// every path, including paths nobody thought about. A lock does not: the coins\n// are still there, the ledger will happily let them be spent again, and the\n// second spend is one this realm cannot honour. Stake 100, then post a 100 bond\n// against the same 100 — the bond transfer succeeds, and at unstake the realm\n// owes 100 CC it does not hold and would have to MINT to return, which breaks\n// supply conservation. That is the one invariant a token cannot lose.\n//\n// The check therefore moved out of the ledger, where it was total, into this\n// realm, where it is a list — and a list can be short one entry. Hence: every\n// path that moves a user's CC into the escrow sizes itself against `spendable`,\n// never against `BalanceOf`, and there is a test that stakes a whole balance and\n// then tries to bond it (TestLockedStakeCannotBeSpentTwice). If a new spend path\n// is ever added, it belongs in that test too.\n\nimport (\n\t\"strconv\"\n\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\n// lockedOf is the CC this address has staked in this court, and therefore may\n// not spend. This is the ONLY reader of the tree: a second one is a second place\n// for the nil-tree and missing-key branches to disagree with the write path.\nfunc lockedOf(c *Court, who address) int64 {\n\tif c.locked == nil { // pre-lock court, or a fixture that built one by hand\n\t\treturn 0\n\t}\n\tif v := c.locked.Get(string(who)); v != nil {\n\t\treturn v.(int64)\n\t}\n\treturn 0\n}\n\n// spendable is what an address may commit in this court: its balance less what\n// it has staked. Locked CC is still in the holder's balance and still votes, so\n// BalanceOf overstates what is available by exactly the staked amount.\nfunc spendable(c *Court, who address) int64 {\n\ts := c.coin.BalanceOf(who) - lockedOf(c, who)\n\tif s \u003c 0 {\n\t\t// Only reachable if a burn ever took coins out from under a lock. It\n\t\t// does not today (nothing burns a user's balance), and clamping is the\n\t\t// conservative reading if it ever does: refuse the spend rather than\n\t\t// let a negative wrap into a large allowance.\n\t\treturn 0\n\t}\n\treturn s\n}\n\n// disposable is what an address may let LEAVE its balance: the balance less\n// whichever claim on it is larger. MAX, not sum, and this is the whole subtlety —\n// a stake lock and a vote lock can cover the same coins (staked CC still votes, so\n// a staker who votes is holding one pile answering to two claims). Adding them\n// would refuse a transfer the holder can honestly make; taking the max refuses\n// exactly the coins that either claim reaches.\nfunc disposable(c *Court, who address) int64 {\n\tclaim := lockedOf(c, who)\n\tif v := voteLockedOf(c, who); v \u003e claim {\n\t\tclaim = v\n\t}\n\td := c.coin.BalanceOf(who) - claim\n\tif d \u003c 0 {\n\t\treturn 0\n\t}\n\treturn d\n}\n\n// DisposableOf is what an address may actually let LEAVE its balance in this court —\n// a bond, a claim deposit, a transfer, a wrap. The balance less whichever claim on it\n// is larger, stake or vote.\n//\n// EXPORTED BECAUSE NOTHING EXPOSED IT, and that was a real gap rather than an\n// omission of convenience: every client had SpendableOf, which is stake-only, and no\n// way at all to read the figure the realm actually enforces. A handoff note asking a\n// web client to \"use the binding limit, not SpendableOf\" was unactionable until this\n// existed.\n//\n// Pairs with SpendableOf, and the two differ exactly when a holder has voted. Quote\n// both or quote this one; quoting only SpendableOf over-promises.\nfunc DisposableOf(courtSlug string, who address) int64 {\n\treturn disposable(mustCourt(courtSlug), who)\n}\n\n// mustSpendable refuses a commitment that would dip into locked stake OR into coin\n// committed by voting. Called by every path that moves a user's CC OUT of their\n// balance — into the escrow or to another holder.\n//\n// NOT called by Stake, which uses mustStakable below. Staking moves nothing: the\n// coin stays in the balance and keeps voting, so a pending vote is no reason to\n// refuse it. Every other caller disposes of coin, and disposal is exactly what the\n// vote lock exists to prevent. The default here is the conservative one on purpose\n// — a new call site that nobody thinks about gets the stricter check.\n/* CC IN THE UNITS A HOLDER READS. Balances are µCC internally, so a refusal that\n   printed one raw would say \"need 8000000\" to somebody whose wallet says 8. Six\n   decimal places, trailing zeros trimmed, so a whole number stays whole and a\n   dusty one keeps only the digits it needs. */\nfunc ccAmt(n int64) string {\n\tif n \u003c 0 {\n\t\treturn \"-\" + ccAmt(-n)\n\t}\n\twhole := strconv.FormatInt(n/1_000_000, 10)\n\tfrac := n % 1_000_000\n\tif frac == 0 {\n\t\treturn whole\n\t}\n\tf := strconv.FormatInt(frac, 10)\n\tfor len(f) \u003c 6 {\n\t\tf = \"0\" + f\n\t}\n\tfor len(f) \u003e 1 \u0026\u0026 f[len(f)-1] == '0' {\n\t\tf = f[:len(f)-1]\n\t}\n\treturn whole + \".\" + f\n}\n\nfunc mustSpendable(c *Court, who address, amount int64) {\n\tif disposable(c, who) \u003e= amount {\n\t\treturn\n\t}\n\t// NAME THE CONSTRAINT THAT ACTUALLY BINDS. One merged message would make every\n\t// refusal read as a stake problem — including the ones that are not — and a\n\t// holder cannot act on \"something is locked\". Three existing tests assert the\n\t// stake wording and they were right to: in their fixtures the stake IS what\n\t// binds, so a merged message would have made them fail for no defect.\n\t// FOUR CASES, not two, and the two that were missing each told the holder\n\t// something false. disposable() subtracts the MAX of the two locks, so on a TIE\n\t// neither lock is the constraint by itself — releasing either one frees nothing,\n\t// which is the opposite of what naming one of them implies. And with NO lock at\n\t// all this fell through to the stake wording, so a holder who has never staked\n\t// was told their stake was in the way when the plain fact is that the balance is\n\t// short. Both were reachable: a transfer above the balance takes the second, and\n\t// staking exactly one's own snapshot and then voting takes the first.\n\t/* THE NUMBERS FIRST. These four read as a paragraph explaining the lock\n\t   model, which is the right content for a doc and the wrong content for a\n\t   refusal: a holder who is short needs to know HOW short, and every one of\n\t   these made them work it out. Reported as too verbose, and it was.\n\t   THE FOUR CASES STAY, because the comment above is still right — a merged\n\t   message would tell somebody their stake is in the way when they have never\n\t   staked. What changed is that each now leads with need-and-have and names\n\t   the binding lock in a clause instead of a sentence. The four prefixes are\n\t   unchanged, which is what the tests match on. */\n\tvl, sl := voteLockedOf(c, who), lockedOf(c, who)\n\tshort := \"need \" + ccAmt(amount) + \" CC, you have \" + ccAmt(disposable(c, who))\n\tswitch {\n\tcase vl \u003c= 0 \u0026\u0026 sl \u003c= 0:\n\t\tpanic(\"kourtv2: not enough CC — \" + short)\n\tcase vl \u003e sl:\n\t\tpanic(\"kourtv2: not enough uncommitted CC — \" + short +\n\t\t\t\" free; \" + ccAmt(vl) + \" is committed by voting\")\n\tcase sl \u003e vl:\n\t\tpanic(\"kourtv2: not enough unstaked CC — \" + short +\n\t\t\t\" free; \" + ccAmt(sl) + \" is staked\")\n\tdefault:\n\t\tpanic(\"kourtv2: not enough free CC — \" + short +\n\t\t\t\" free; \" + ccAmt(sl) + \" is both staked and committed by voting\")\n\t}\n}\n\n// mustStakable refuses a stake that would dip into locked stake, and deliberately\n// ignores the vote lock. See mustSpendable for why the two differ.\nfunc mustStakable(c *Court, who address, amount int64) {\n\tif s := spendable(c, who); s \u003c amount {\n\t\t// The staked clause only when something IS staked: with no lock at all\n\t\t// the plain fact is that the balance is short, and saying otherwise is\n\t\t// the same falsehood the four-case switch above exists to avoid.\n\t\ttail := \"\"\n\t\tif l := lockedOf(c, who); l \u003e 0 {\n\t\t\ttail = \"; \" + ccAmt(l) + \" is staked\"\n\t\t}\n\t\tpanic(\"kourtv2: not enough unstaked CC — need \" + ccAmt(amount) +\n\t\t\t\" CC, you have \" + ccAmt(s) + tail)\n\t}\n}\n\n// lockStake records a claim on `amount` of who's balance. Paired with exactly one\n// releaseStake per unit: the three sites that move stakePos.stake are Stake,\n// Unstake and the settlement withdrawal, and no other path may touch this tree.\nfunc lockStake(c *Court, who address, amount int64) {\n\tif amount \u003c= 0 {\n\t\treturn\n\t}\n\tif c.locked == nil {\n\t\tc.locked = bptree.NewBPTree32()\n\t}\n\tc.locked.Set(string(who), mustAdd(lockedOf(c, who), amount))\n}\n\n// releaseStake drops a claim previously recorded. Panics rather than clamping if\n// asked to release more than is locked: that would mean stakePos.stake and this\n// tree had already diverged, and continuing would hand back CC the holder never\n// committed. The panic is the conservation check.\nfunc releaseStake(c *Court, who address, amount int64) {\n\tif amount \u003c= 0 {\n\t\treturn\n\t}\n\tl := lockedOf(c, who)\n\tif amount \u003e l {\n\t\tpanic(\"kourtv2: releasing more stake than is locked\")\n\t}\n\tif amount == l {\n\t\tc.locked.Remove(string(who)) // no row for a zero — state costs rent\n\t\treturn\n\t}\n\tc.locked.Set(string(who), l-amount)\n}\n\n// LockedOf is the read for clients and for the position page: how much of an\n// address's CC is committed as stake in this court. The complement — what they can\n// still STAKE — is SpendableOf. What they can still bond, deposit, transfer or wrap\n// is DisposableOf, which is a different and usually smaller number.\nfunc LockedOf(courtSlug string, who address) int64 {\n\treturn lockedOf(mustCourt(courtSlug), who)\n}\n\n// SpendableOf is what an address may still STAKE. It is NOT what they may spend, and\n// the name predates the vote lock.\n//\n// Only Stake draws on this figure, through mustStakable. Every other commitment — a\n// bond, a claim deposit, a transfer, a wrap — draws on DisposableOf through\n// mustSpendable, which nets out the vote lock as well as the stake lock. So a client\n// showing this as \"what you can commit\" over-promises to any holder who has voted,\n// and the realm will refuse at signing.\n//\n// That is not hypothetical: render.gno's wallet panel printed exactly this figure\n// under \"what a new stake, deposit or bond draws on\" until it was split into two\n// rows. A single number cannot answer two gates.\nfunc SpendableOf(courtSlug string, who address) int64 {\n\treturn spendable(mustCourt(courtSlug), who)\n}\n\n// TransferCC moves court coin from the caller to another holder of the same\n// court. courtSlug names the court, `to` is the recipient, `amount` is in CC\n// base units.\n//\n// ONLY UNCOMMITTED COIN MOVES. The ceiling is DisposableOf(courtSlug, caller):\n// the balance less whichever claim on it is larger, the stake lock or the vote\n// lock. Coin behind a live stake, an answer bond, a dispute bond, a flag bond,\n// a claim deposit, an election bond, or a vote still in flight cannot be sold\n// out from under the commitment it is backing. LockedOf, VoteLockedOf and\n// CommitmentsOf say which lock binds and by how much.\n//\n// Refused when the court does not exist, `amount` is zero or negative, `to` is\n// the caller, either end is the court escrow — the court's own account, never a\n// transfer endpoint in either direction — or `amount` exceeds DisposableOf.\n//\n// WHAT A TRANSFER DOES NOT CARRY. Reputation stays with the address that earned\n// it. A claim position stays too: conviction accrues to whoever held the stake\n// over time, so buying coin buys no position. And this is a market between\n// holders, not a redemption — no GNOT is paid out, the curve stays one-way, and\n// its payment stays burned.\nfunc TransferCC(cur realm, courtSlug string, to address, amount int64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tif amount \u003c= 0 {\n\t\tpanic(\"kourtv2: transfer amount must be positive\")\n\t}\n\tfrom := cur.Previous().Address()\n\tif from == to {\n\t\tpanic(\"kourtv2: cannot transfer to yourself\")\n\t}\n\tc := mustCourt(courtSlug)\n\t// The escrow is the realm's own account: bonds and deposits live there and\n\t// are disposed only by the lanes that own them. It must never be a transfer\n\t// endpoint in either direction.\n\tif to == c.escrow || from == c.escrow {\n\t\tpanic(\"kourtv2: the court escrow is not a transfer endpoint\")\n\t}\n\ttouch(c)\n\tmustSpendable(c, from, amount)\n\tc.coin.Transfer(from, to, amount)\n}\n\n// ApproveCC lets `spender` move up to `amount` of the caller's coin in this\n// court, through TransferFromCC. `amount` is in CC base units.\n//\n// SETS the allowance, it does not add to it: the new figure replaces whatever\n// was there, and 0 revokes.\n//\n// APPROVING RESERVES NOTHING AND LOCKS NOTHING. There is no balance check here.\n// The gate is at the spend: TransferFromCC caps the move at DisposableOf(owner)\n// as it stands at that moment. So an allowance may exceed what the owner can\n// actually move, and the spend then simply fails. Read a granted allowance as a\n// ceiling, never as available capital — AllowanceCC returns that ceiling.\n//\n// A spender can do strictly less than the owner could do with TransferCC. No\n// authority is granted, only permission to move coin the owner could have moved\n// themselves.\n//\n// Refused when the court does not exist, `amount` is negative, `spender` is not\n// a valid address, or either the caller or `spender` is the court escrow.\nfunc ApproveCC(cur realm, courtSlug string, spender address, amount int64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tif amount \u003c 0 {\n\t\tpanic(\"kourtv2: negative allowance\")\n\t}\n\tc := mustCourt(courtSlug)\n\towner := cur.Previous().Address()\n\t// Symmetric with TransferCC: the escrow is not an endpoint, so it must not\n\t// be reachable as a spender either. Otherwise an allowance naming it would\n\t// sit there looking like a claim on escrowed coin.\n\tif spender == c.escrow || owner == c.escrow {\n\t\tpanic(\"kourtv2: the court escrow is not a transfer endpoint\")\n\t}\n\ttouch(c)\n\tc.coin.Approve(owner, spender, amount)\n}\n\n// TransferFromCC spends an allowance the owner granted the caller.\n//\n// Every guard TransferCC applies applies here, keyed on `from` rather than on\n// the caller — mustSpendable in particular. An allowance is not a lock and never\n// outranks one: if the owner has since committed the coin to a stake, a bond or\n// a deposit, the spend fails, and it fails for the spender exactly as it would\n// have failed for the owner. That is the property that makes the allowance safe\n// to hand out at all, and it is why the check is on `from`: gating on the spender\n// would gate the wrong balance and leave the owner's lock unenforced.\nfunc TransferFromCC(cur realm, courtSlug string, from, to address, amount int64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tif amount \u003c= 0 {\n\t\tpanic(\"kourtv2: transfer amount must be positive\")\n\t}\n\tif from == to {\n\t\tpanic(\"kourtv2: cannot transfer to yourself\")\n\t}\n\tc := mustCourt(courtSlug)\n\tif to == c.escrow || from == c.escrow {\n\t\tpanic(\"kourtv2: the court escrow is not a transfer endpoint\")\n\t}\n\ttouch(c)\n\tmustSpendable(c, from, amount)\n\tc.coin.TransferFrom(cur.Previous().Address(), from, to, amount)\n}\n\n// AllowanceCC reports what `spender` may still move of `owner`'s court coin.\n//\n// Read it as a ceiling, not as a balance: the spend is additionally capped by\n// disposable(owner) at the moment it happens, so the amount actually movable is\n// min(AllowanceCC, DisposableOf).\n//\n// NOT SpendableOf, which this comment named until it was checked. SpendableOf is\n// stake-only, so quoting it here over-promises by the whole of a vote commitment —\n// the exact over-promise the SpendableOf doc thirty lines above warns against.\n// TestAnAllowanceCannotOutrunAVoteCommitment pins the spender being refused for that\n// reason, with the uncommitted remainder still moving.\nfunc AllowanceCC(courtSlug string, owner, spender address) int64 {\n\treturn mustCourt(courtSlug).coin.Allowance(owner, spender)\n}\n"},{"name":"media.gno","body":"package kourtv2\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"strings\"\n\n\tsanitize \"gno.land/p/nt/markdown/sanitize/v0\"\n)\n\n// mediaDestination is the ONE url gnoweb's markdown will point at, and it is the\n// archive's whenever there is an archive to point at.\n//\n// A markdown image has a single destination and no `onerror`, so this surface\n// has no fallback chain: whichever host is named here is the host every reader's\n// browser contacts. Naming a mirror the FILER chose would make every claim page\n// a small amplifier aimed wherever they liked, at readers who — on gnoweb, whose\n// CSP forbids the fetch — cannot verify what comes back either.\n//\n// So: the archive, derived from the hash on the domain the admin configured.\n// The cost is honest. If the archive does not hold the bytes the image is broken\n// here, while kourt.xyz still finds it through a mirror and can prove it matches.\n// A broken image on the surface that cannot verify beats a working one that\n// cannot be trusted and discloses its readers.\n//\n// Video is the exception, and not by preference: the archive does not mirror it,\n// so a video item has nowhere to point but the host that streams it.\nfunc mediaDestination(m *mediaItem) string {\n\tif m.kind == mediaKindImage \u0026\u0026 m.sha256 != \"\" \u0026\u0026\n\t\tsiteDomain != \"\" \u0026\u0026 siteDomainFault(siteDomain) == \"\" {\n\t\treturn \"https://\" + siteDomain + \"/m/\" + m.sha256\n\t}\n\tfor _, u := range m.mirrors {\n\t\t// Re-asked here as well as at the item level, so a mirror whose host has\n\t\t// left the allowlist is never the one a page points at.\n\t\tif mirrorFault(u) == \"\" {\n\t\t\treturn u\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// writeClaimMedia puts the claim's evidence on the page, under the body.\n//\n// EVERY EXHIBIT IS NUMBERED, AND THE NUMBER IS VISIBLE. PurgeClaimMedia takes a\n// zero-based index, and without a number on the page a moderator taking down the\n// third exhibit would be counting positions in a JSON payload. The reader wants\n// it too: \"the second exhibit\" is how people argue about evidence.\n//\n// A purged or unavailable exhibit KEEPS ITS PLACE and says what happened. A gap\n// that silently renumbered the images around it would leave every later exhibit\n// referring to something else than it did yesterday, in the middle of an\n// argument about which one shows what.\n// verifyElsewhere names the surface that CAN check an exhibit, when there is one\n// to name. Built from the same configured domain the archive lives on, so the\n// two can never disagree about where the overlay is.\nfunc verifyElsewhere() string {\n\tif siteDomain == \"\" || siteDomainFault(siteDomain) != \"\" {\n\t\treturn \"\"\n\t}\n\treturn \" [\" + siteDomain + \"](https://\" + siteDomain + \") checks it.\"\n}\n\nfunc writeClaimMedia(b *strings.Builder, c *Court, cs *claimState) {\n\titems := claimMediaVisible(c, cs)\n\tif len(items) == 0 {\n\t\treturn\n\t}\n\ttotal := strconv.Itoa(len(items))\n\tb.WriteString(\"## Evidence filed with this claim\\n\\n\")\n\t// WHAT THIS PAGE CANNOT TELL YOU, said before the pictures rather than after.\n\t//\n\t// The whole value of filing a hash is that a swapped image is detectable —\n\t// and this surface cannot perform that check, because gnoweb's CSP forbids\n\t// fetching a third-party image at all. A reader shown evidence with no\n\t// mention of that would reasonably assume the court had vouched for it.\n\t// Silence here is not neutral; it is a claim the court cannot support.\n\tb.WriteString(\"_The court recorded a fingerprint of each image when the claim \" +\n\t\t\"was filed, so a swap can be detected — but not on this page._\" + verifyElsewhere() + \"\\n\\n\")\n\tfor i := range items {\n\t\tm := \u0026items[i]\n\t\tpos := strconv.Itoa(i+1) + \" of \" + total\n\t\tif m.purged {\n\t\t\tb.WriteString(\"_Exhibit \" + pos + \" was taken down._\\n\\n\")\n\t\t\tcontinue\n\t\t}\n\t\tdest := mediaDestination(m)\n\t\tif dest == \"\" {\n\t\t\tb.WriteString(\"_Exhibit \" + pos + \" is not currently available._\\n\\n\")\n\t\t\tcontinue\n\t\t}\n\t\t// The caption is author-written, so it goes through InlineText — the\n\t\t// right tool for text, exactly as it is the wrong tool for the URL on the\n\t\t// same line, which is emitted raw because it was validated fail-closed.\n\t\tcaption := \"\"\n\t\tif m.caption != \"\" {\n\t\t\tcaption = \" — \" + sanitize.InlineText(m.caption)\n\t\t}\n\t\tif m.kind == mediaKindVideo {\n\t\t\t// LINKED, NOT VERIFIED, and it has to say so. A streaming host serves\n\t\t\t// no stable bytes to commit to, so nothing can check this the way an\n\t\t\t// image is checked; giving it the same chrome as a verified exhibit\n\t\t\t// would drain the distinction of meaning everywhere it appears.\n\t\t\tb.WriteString(\"[▶ Exhibit \" + pos + caption + \"](\" + dest +\n\t\t\t\t\") _(a link; the court holds no copy and cannot vouch for it)_\\n\\n\")\n\t\t\tcontinue\n\t\t}\n\t\t// The alt text carries the POSITION only, because the caption is printed\n\t\t// visibly on the next line. Repeating it makes a screen reader say the\n\t\t// same words twice, and makes a broken image show them twice.\n\t\tb.WriteString(\"![Exhibit \" + pos + \"](\" + dest + \")\\n\\n\")\n\t\tb.WriteString(\"_Exhibit \" + pos + caption + \"_\\n\\n\")\n\t}\n}\n\n// ClaimMediaPage answers with the evidence of a RUN of claims in one read.\n//\n// WHY A BATCH EXISTS AT ALL. The map draws a node per claim and wants a\n// thumbnail on each, and the overlay has no bulk claim fetch — it issues a read\n// per claim. Asking per claim for media too would mean fifty extra round trips\n// before a fifty-claim map could draw, and the map is deliberately built to\n// paint before its bodies arrive. One read for the page it is drawing keeps\n// that promise.\n//\n// Every item goes through claimMediaVisible, the same gate the single read uses,\n// so a purged claim answers with nothing here exactly as it does there. A batch\n// that skipped the gate would be a way to read past a purge fifty at a time.\n//\n// A claim that does not exist contributes an empty array rather than aborting:\n// the caller is drawing a range, and one hole in it must not cost the page.\n// count is clamped so a caller cannot ask for the whole docket in one read.\nfunc ClaimMediaPage(courtSlug string, fromID uint64, count int) string {\n\tc := mustCourt(courtSlug)\n\tif count \u003c 1 {\n\t\treturn \"[]\"\n\t}\n\tif count \u003e maxMediaPage {\n\t\tcount = maxMediaPage\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"[\")\n\tfor i := 0; i \u003c count; i++ {\n\t\tif i \u003e 0 {\n\t\t\tb.WriteString(\",\")\n\t\t}\n\t\tid := fromID + uint64(i)\n\t\tv := c.claims.Get(beClaimKey(id))\n\t\tif v == nil {\n\t\t\tb.WriteString(\"[]\")\n\t\t\tcontinue\n\t\t}\n\t\tb.WriteString(encodeMedia(claimMediaVisible(c, v.(*claimState))))\n\t}\n\tb.WriteString(\"]\")\n\treturn b.String()\n}\n\n// PurgeClaimMedia destroys one media item on a claim. Global DAO members only,\n// at the purge threshold, with a statutory category — the same gate every other\n// purge verb in this realm carries, including the panic string verbatim, because\n// check-epoch-coherence requires every purge to speak with one voice so a new\n// one cannot drift its own authorization past the census.\n//\n// WHAT IS DESTROYED. The hash, the mirrors and the caption: the item's whole\n// payload, zeroed rather than flagged, exactly as PurgeBoardRow zeroes a\n// comment's text. The realm never held the bytes, so this cannot unpublish them\n// — what it removes is the court's pointer to them and the court's word that\n// they are evidence. Taking the bytes down is the archive's job (CLAIM_MEDIA\n// §3.2), and the two are deliberately independent: either alone suffices to\n// remove an image from the overlay, and neither can rewrite what was filed.\n//\n// THE SLOT SURVIVES, tombstoned in place. Removing the element would shift every\n// later index, so a moderator working through seven items would find their\n// second call landing on one they had already reviewed and kept.\n//\n// idx is zero-based, and a repeat is a no-op rather than an error — checked\n// before the vote, so it consumes no m-of-n proposal and writes no second log\n// row for an act that cannot change anything.\nfunc PurgeClaimMedia(cur realm, courtSlug string, claimID uint64, idx uint64, categoryCode string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\td := ensureGlobalDAO()\n\tif !d.members.Has(who.String()) {\n\t\tpanic(\"kourtv2: only a global DAO member may purge\")\n\t}\n\tmustCategoryCode(categoryCode)\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif idx \u003e= uint64(len(cs.media)) {\n\t\tpanic(\"kourtv2: this claim carries no media item at that position\")\n\t}\n\tif cs.media[idx].purged {\n\t\treturn\n\t}\n\tfire, votedCode := approveAction(d.pending,\n\t\t\"mediapurge:\"+c.id+\":\"+strconv.FormatUint(claimID, 10)+\":\"+strconv.FormatUint(idx, 10),\n\t\twho, categoryCode, d.purgeM)\n\tif !fire {\n\t\treturn\n\t}\n\tm := \u0026cs.media[idx]\n\tm.purged = true\n\tm.sha256 = \"\"\n\tm.mirrors = nil\n\tm.caption = \"\"\n\tm.mime = \"\"\n\t// boardActCode's shape, not a second one. Every row-scoped purge in this realm\n\t// logs as verb#id:code, and the `#` is what says the number is the HANDLE the\n\t// verb takes rather than another code beside it. modActLine prints this\n\t// straight to a page a person reads, and the file that prints it states the\n\t// rule: a power addressed by an id nobody can see is a power nobody can use.\n\t// An id shown in a shape nobody recognises is the same failure, one step on.\n\tensureClaimMod(c, ensureMod(c), claimID).\n\t\tappendLog(who, boardActCode(\"media-purge\", idx, votedCode), \"\")\n\temitPurge(c.id, claimID, \"media-item\", who)\n}\n\n// MEDIA A CLAIM CARRIES, AND THE PROOF IT HAS NOT CHANGED. docs/CLAIM_MEDIA.md\n// is the design record; this file is the chain's half of it.\n//\n// THE REALM NEVER SEES THE BYTES. It stores a sha256 the author committed to at\n// filing time and a list of places the bytes may be found. That is the whole\n// idea, and it answers two things a bare URL could not:\n//\n//   - A host that deletes the image cannot delete the evidence. Anyone may\n//     re-serve the same bytes, and the hash proves they are the same bytes.\n//   - The author cannot swap the bytes afterward. A URL's contents are mutable;\n//     a hash is not. Without this, an author could take a position, watch the\n//     market, and re-point the image — which is the exact attack the body's\n//     append-only rule exists to prevent, through a door the body never opened.\n//\n// The hash is a COMMITMENT, not a proof of content: the realm cannot check it,\n// and a wrong one only makes the author's own image fail to render everywhere.\nconst (\n\tmaxClaimMediaCount = 7\n\tmaxMirrorsPerItem  = 4\n\tmaxMediaURLLen     = 300\n\tmaxCaptionLen      = 120\n\t// 256 KiB. The composer downscales to fit this without ever telling anyone\n\t// about it (CLAIM_MEDIA §2.2), so the cap is a backstop, not a user-facing\n\t// rule. It also bounds what a hostile mirror can stream at a viewer.\n\tmaxMediaBytes = 262144\n\tmaxMediaDim   = 20000\n\t// One map draw's worth of claims, and a ceiling on what a single read can\n\t// cost the node. The overlay pages its docket well below this.\n\tmaxMediaPage = 64\n)\n\n// mediaKind is spelled rather than inferred. A video item has no hash — a\n// streaming host serves no stable bytes to commit to — and inferring \"no hash\n// means video\" would silently turn a malformed image item into a video one.\nconst (\n\tmediaKindImage = \"img\"\n\tmediaKindVideo = \"vid\"\n)\n\ntype mediaItem struct {\n\tkind    string // mediaKindImage | mediaKindVideo\n\tsha256  string // 64 lowercase hex; empty for video\n\tmime    string\n\tw, h    int\n\tbytes   int\n\tcaption string\n\tmirrors []string\n\t// purged tombstones the slot IN PLACE. Removing the element would shift\n\t// every later index, so a moderator working through seven items would have\n\t// their second call land on one they had already reviewed and kept.\n\tpurged bool\n}\n\n// mediaCharFault rejects anything that has no business in a URL and would cost\n// us dearly somewhere downstream. The parentheses are the ones worth naming: a\n// mirror is emitted into a markdown image destination, `![alt](url)`, and a `)`\n// inside it closes that destination early and spills the remainder onto the page\n// as text. That is the same class of bug as the `kourt\\.xyz` escape that broke\n// the site link — a string put through the wrong text machinery. The answer is\n// the same as it was there: validate fail-closed on write, emit raw, and never\n// reach for sanitize.InlineText on a URL, which escapes `.` `-` `_` `(` `)` and\n// would turn every mirror into a corpse.\n//\n// `,` and `|` are excluded because they are the wire format's separators, and a\n// URL carrying one would let a single field become two.\nfunc mediaCharFault(s string) string {\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tc := s[i]\n\t\tif c \u003c 0x21 || c \u003e 0x7e {\n\t\t\treturn \"a mirror is printable ASCII with no spaces\"\n\t\t}\n\t\tswitch c {\n\t\tcase '\"', '\\'', '\u003c', '\u003e', '(', ')', '\\\\', ',', '|', '`':\n\t\t\treturn \"a mirror may not contain \" + string(rune(c))\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc isHexLower(s string) bool {\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tc := s[i]\n\t\tif (c \u003e= '0' \u0026\u0026 c \u003c= '9') || (c \u003e= 'a' \u0026\u0026 c \u003c= 'f') {\n\t\t\tcontinue\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\n// mirrorFault is split from any panic for siteDomainFault's reason: the RENDER\n// path must be able to ask the same question without being able to abort a page.\n// A bad stored value costs the image, never the claim underneath it.\n//\n// This matters more here than it does for the site domain, because the host\n// allowlist can narrow after a value is stored. Render re-asks, so retiring a\n// host retracts the images already pointing at it instead of grandfathering them.\nfunc mirrorFault(u string) string {\n\tif len(u) == 0 || len(u) \u003e maxMediaURLLen {\n\t\treturn \"a mirror is 1..\" + strconv.Itoa(maxMediaURLLen) + \" characters\"\n\t}\n\tif fault := mediaCharFault(u); fault != \"\" {\n\t\treturn fault\n\t}\n\tconst scheme = \"https://\"\n\tif !strings.HasPrefix(u, scheme) {\n\t\treturn \"a mirror is https — plain http is blocked as mixed content\"\n\t}\n\t// The authority ends at the FIRST of '/', '?' or '#'. Cutting only at '/'\n\t// would read \"i.imgur.com?v=2\" as the host and refuse a URL whose query\n\t// follows the host directly — fail-closed, but refused for a reason nobody\n\t// could act on. Cutting at all three also keeps a fragment from smuggling a\n\t// trusted-looking name past the check: in \"https://evil.com/#i.imgur.com\"\n\t// the host is and stays evil.com.\n\thost := u[len(scheme):]\n\tfor _, sep := range []byte{'/', '?', '#'} {\n\t\tif i := strings.IndexByte(host, sep); i \u003e= 0 {\n\t\t\thost = host[:i]\n\t\t}\n\t}\n\tif host == \"\" {\n\t\treturn \"a mirror needs a host\"\n\t}\n\tif !mediaHostAllowed(host) {\n\t\treturn \"the browser will not load images from \" + host +\n\t\t\t\"; see the allowed hosts on the help page\"\n\t}\n\treturn \"\"\n}\n\n// THE HOST ALLOWLIST MIRRORS GNOWEB'S CSP, AND HAS TO.\n//\n// An image only loads if the browser's Content-Security-Policy permits its host,\n// and a blocked image is a broken glyph on the page with NO error signal back to\n// the realm — the chain would believe it had published something the reader\n// cannot see. So the realm refuses at write time what the browser would refuse\n// at load time, and the reader gets a message from the court instead of silence.\n//\n// The list tracks gnoweb's `cspImgHost`. Drift is the standing hazard: if gnoweb\n// narrows its policy, claims pointing at a dropped host break, and nothing here\n// will know — which is why this is a PARAMETER rather than a constant (owner\n// ruling, CLAIM_MEDIA §10.1). A const cannot be corrected without redeploying\n// the realm, and the list it tracks belongs to somebody else.\n//\n// The defaults below are what ships. They are also what check-media-hosts.py\n// compares against web/media.js and the page's CSP, because they are what a\n// fresh deployment and every offline copy actually use.\nvar defaultMediaHostsExact = []string{\n\t\"gnolang.github.io\",\n\t\"assets.gnoteam.com\",\n\t\"sa.gno.services\",\n\t\"imgur.com\",\n\t\"github.com\",\n\t\"imgflip.com\",\n\t\"ipfs.io\",\n\t\"cloudflare-ipfs.com\",\n}\n\nvar defaultMediaHostSuffixes = []string{\n\t\".imgur.com\",\n\t\".github.io\",\n\t\".githubusercontent.com\",\n\t\".imgflip.com\",\n}\n\n// The live lists. Copied rather than aliased so a later Set cannot be observed\n// to have edited the defaults the guard reads.\nvar mediaHostsExact = appendAll(nil, defaultMediaHostsExact)\nvar mediaHostSuffixes = appendAll(nil, defaultMediaHostSuffixes)\n\nfunc appendAll(dst, src []string) []string {\n\tfor _, v := range src {\n\t\tdst = append(dst, v)\n\t}\n\treturn dst\n}\n\n// maxMediaHosts bounds each list. Every entry is checked on every mirror, on\n// every write AND on every render, so an unbounded list is an unbounded cost\n// paid by readers; and a list nobody can read through is not a policy anyone can\n// audit.\nconst maxMediaHosts = 32\n\n// mediaHostEntryFault is the shape of one entry. Deliberately narrow: these\n// become part of a Content-Security-Policy comparison, and a host with a scheme,\n// a port, a path or a wildcard in it would compare against something the browser\n// never sees.\nfunc mediaHostEntryFault(h string, suffix bool) string {\n\tif h == \"\" {\n\t\treturn \"a host may not be empty\"\n\t}\n\tif len(h) \u003e 100 {\n\t\treturn \"a host is at most 100 characters\"\n\t}\n\tif suffix \u0026\u0026 h[0] != '.' {\n\t\treturn \"a suffix begins with a dot: \" + h\n\t}\n\tif !suffix \u0026\u0026 h[0] == '.' {\n\t\treturn \"an exact host does not begin with a dot: \" + h\n\t}\n\tfor i := 0; i \u003c len(h); i++ {\n\t\tc := h[i]\n\t\tok := (c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= '0' \u0026\u0026 c \u003c= '9') || c == '.' || c == '-'\n\t\tif !ok {\n\t\t\treturn \"a host is lowercase letters, digits, dots and hyphens: \" + h\n\t\t}\n\t}\n\t// \"..\" would make an empty label, and a trailing dot compares against nothing.\n\tif strings.Contains(h, \"..\") || h[len(h)-1] == '.' {\n\t\treturn \"a host has no empty label: \" + h\n\t}\n\treturn \"\"\n}\n\n// parseMediaHosts turns one comma-separated list into entries, refusing the\n// whole list if any entry is wrong. Partial acceptance would leave an admin\n// believing they had set something they had not.\nfunc parseMediaHosts(list string, suffix bool) []string {\n\tif strings.TrimSpace(list) == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(list, \",\")\n\tif len(parts) \u003e maxMediaHosts {\n\t\tpanic(\"kourtv2: at most \" + strconv.Itoa(maxMediaHosts) + \" hosts in a list\")\n\t}\n\tout := []string{}\n\tfor _, p := range parts {\n\t\th := strings.TrimSpace(p)\n\t\tif fault := mediaHostEntryFault(h, suffix); fault != \"\" {\n\t\t\tpanic(\"kourtv2: \" + fault)\n\t\t}\n\t\tfor _, seen := range out {\n\t\t\tif seen == h {\n\t\t\t\tpanic(\"kourtv2: \" + h + \" is listed twice\")\n\t\t\t}\n\t\t}\n\t\tout = append(out, h)\n\t}\n\treturn out\n}\n\n// SetMediaHosts replaces the hosts a claim's evidence may be mirrored on. Global\n// DAO admin only, the same seat that sets the site domain.\n//\n// THIS IS ALSO THE HOST KILL SWITCH (§10.4, answered by §10.1). Removing a host\n// takes effect on every existing claim immediately, because mediaDestination\n// re-asks mirrorFault on every render rather than trusting what passed at write\n// time. No purge, no migration: the next read of any claim page stops pointing\n// at that host.\n//\n// WHAT IT CANNOT DO, and an admin must know it: this list is one half of a pair.\n// The other half is the Content-Security-Policy served by gnoweb and by the\n// overlay's own nginx, and neither is on chain. Adding a host here lets the realm\n// STORE a mirror there; whether a browser will LOAD it is still the CSP's\n// decision. Widening this without widening those produces exactly the silent\n// broken image the whole list exists to prevent.\nfunc SetMediaHosts(cur realm, exact, suffixes string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\td := ensureGlobalDAO()\n\tif cur.Previous().Address() != d.admin {\n\t\tpanic(\"kourtv2: only the global DAO admin sets the media hosts\")\n\t}\n\tex := parseMediaHosts(exact, false)\n\tsf := parseMediaHosts(suffixes, true)\n\tif len(ex) == 0 \u0026\u0026 len(sf) == 0 {\n\t\t// Emptying the list is a legitimate act — it stops every mirror the realm\n\t\t// does not host itself — but it is drastic enough to be said out loud\n\t\t// rather than reached by passing two blank strings.\n\t\tpanic(\"kourtv2: to allow no third-party host, call ClearMediaHosts\")\n\t}\n\tmediaHostsExact = ex\n\tmediaHostSuffixes = sf\n\tchain.Emit(globalActEvent,\n\t\t\"court\", \"*\",\n\t\t\"claim\", \"0\",\n\t\t\"act\", \"set-media-hosts:\"+strconv.Itoa(len(ex))+\"+\"+strconv.Itoa(len(sf)),\n\t\t\"by\", cur.Previous().Address().String(),\n\t\t\"height\", eventHeight(),\n\t)\n}\n\n// ClearMediaHosts allows no third-party host at all: only the archive on the\n// configured site domain, which mediaHostAllowed permits without listing. Same\n// seat, and named separately because it is the one setting that cannot be\n// arrived at by accident.\nfunc ClearMediaHosts(cur realm) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\td := ensureGlobalDAO()\n\tif cur.Previous().Address() != d.admin {\n\t\tpanic(\"kourtv2: only the global DAO admin sets the media hosts\")\n\t}\n\tmediaHostsExact = nil\n\tmediaHostSuffixes = nil\n\tchain.Emit(globalActEvent,\n\t\t\"court\", \"*\",\n\t\t\"claim\", \"0\",\n\t\t\"act\", \"clear-media-hosts\",\n\t\t\"by\", cur.Previous().Address().String(),\n\t\t\"height\", eventHeight(),\n\t)\n}\n\n// MediaHosts is what the overlay reads so its own refusals match this realm's.\n//\n// ONE LINE, exact and suffixes separated by a bar, each list comma-separated —\n// the overlay's parseTyped splits raw output on newlines before it matches a\n// typed value, so a multi-line answer parses as garbage.\nfunc MediaHosts() string {\n\treturn strings.Join(mediaHostsExact, \",\") + \"|\" + strings.Join(mediaHostSuffixes, \",\")\n}\n\n// mediaHostAllowed. The overlay's own domain is allowed WITHOUT being listed\n// here, because it is not a constant: kourt.xyz keeps a copy of every image at\n// an address derived from the hash, and the domain that archive lives on is the\n// one the admin configured. Deriving it from siteDomain rather than hardcoding\n// means the archive and the reader-facing site can never disagree about where\n// the overlay is.\nfunc mediaHostAllowed(host string) bool {\n\tif host == \"\" {\n\t\treturn false\n\t}\n\tif siteDomain != \"\" \u0026\u0026 host == siteDomain \u0026\u0026 siteDomainFault(siteDomain) == \"\" {\n\t\treturn true\n\t}\n\tfor _, h := range mediaHostsExact {\n\t\tif host == h {\n\t\t\treturn true\n\t\t}\n\t}\n\tfor _, s := range mediaHostSuffixes {\n\t\t// A suffix match must not accept the bare suffix as a whole host:\n\t\t// \".github.io\" would otherwise let \"github.io\" through, and a longer\n\t\t// host is required for the label to mean anything.\n\t\tif len(host) \u003e len(s) \u0026\u0026 strings.HasSuffix(host, s) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// captionFault. A caption is the exhibit label AND the alt text, which is why it\n// is worth carrying an extra author-written string: \"Exhibit A — the email\n// header\" is better evidence and better accessibility than a generated\n// \"image 1 of 3\".\n//\n// It is claim text (owner ruling, CLAIM_MEDIA §10.6), so it takes the body's\n// rule whole — fixed at creation, no editor, gated by the same purge. Newlines\n// and the separators are refused so one caption can never become two fields or\n// two items.\nfunc captionFault(s string) string {\n\t// COUNTED IN CHARACTERS, NOT BYTES, because that is what the message says and\n\t// what the person can count. len(s) is UTF-8 bytes: an ordinary 82-character\n\t// Russian caption is 152 of them, so the realm refused it while web/media.js\n\t// — which counts code points — waved it through. The result was a claim that\n\t// could not be filed, aborting with \"a caption is at most 120 characters\"\n\t// about a caption of 82. Any non-Latin script hit this at roughly half the\n\t// stated limit; the two sides had never been compared on a non-ASCII string.\n\tif runeLen(s) \u003e maxCaptionLen {\n\t\treturn \"a caption is at most \" + strconv.Itoa(maxCaptionLen) + \" characters\"\n\t}\n\t// The bidi controls, and only the OVERRIDES, EMBEDDINGS and ISOLATES.\n\t//\n\t// The rule below already says a caption carries no control characters, and\n\t// meant it — but it read bytes, and these are three bytes each, so it never\n\t// saw one. A caption is claim text: permanent, unamendable, and printed\n\t// beside the exhibit number on a page a stranger reads. U+202E reorders what\n\t// follows it, so a filer could make the rendered line disagree with the line\n\t// the chain stores, which is the one property this record exists to have.\n\t//\n\t// The MARKS (U+200E/200F) are deliberately still allowed: ordinary Arabic and\n\t// Hebrew need them, and they carry no override power. Refusing those would\n\t// break honest captions to stop nothing.\n\tfor _, r := range s {\n\t\tif (r \u003e= 0x202A \u0026\u0026 r \u003c= 0x202E) || (r \u003e= 0x2066 \u0026\u0026 r \u003c= 0x2069) {\n\t\t\treturn \"a caption may not contain text-direction controls\"\n\t\t}\n\t}\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tswitch s[i] {\n\t\tcase '\\n', '\\r':\n\t\t\treturn \"a caption is one line\"\n\t\tcase '|':\n\t\t\t// UNREACHABLE THROUGH parseMediaFault, and kept anyway. The caption is\n\t\t\t// field seven of eight, so a bar in it makes nine fields and the split\n\t\t\t// refuses the line before this is asked. This stands for a future\n\t\t\t// caller that sets a caption some other way — and it is documented as\n\t\t\t// belt and braces so nobody mistakes it for the gate that is holding.\n\t\t\treturn \"a caption may not contain a vertical bar\"\n\t\t}\n\t\tif s[i] \u003c 0x20 \u0026\u0026 s[i] != '\\t' {\n\t\t\treturn \"a caption may not contain control characters\"\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// mediaItemFault validates one parsed item. Returns \"\" when the item is usable.\nfunc mediaItemFault(m *mediaItem) string {\n\tswitch m.kind {\n\tcase mediaKindImage:\n\t\tif len(m.sha256) != 64 || !isHexLower(m.sha256) {\n\t\t\treturn \"an image needs a sha256: 64 lowercase hex characters\"\n\t\t}\n\t\tif m.w \u003c= 0 || m.h \u003c= 0 || m.w \u003e maxMediaDim || m.h \u003e maxMediaDim {\n\t\t\treturn \"an image needs real dimensions\"\n\t\t}\n\t\tif m.bytes \u003c= 0 || m.bytes \u003e maxMediaBytes {\n\t\t\treturn \"an image is 1..\" + strconv.Itoa(maxMediaBytes) + \" bytes\"\n\t\t}\n\t\tif m.mime == \"\" {\n\t\t\treturn \"an image needs a media type\"\n\t\t}\n\tcase mediaKindVideo:\n\t\t// No hash: a streaming host serves no stable bytes to commit to. The\n\t\t// consequence is carried honestly to the reader — a video is LINKED, not\n\t\t// verified — because giving an unverifiable item the same chrome as a\n\t\t// verified one would drain the badge of meaning everywhere it appears.\n\t\tif m.sha256 != \"\" {\n\t\t\treturn \"a video link carries no hash\"\n\t\t}\n\tdefault:\n\t\treturn \"a media item is \" + mediaKindImage + \" or \" + mediaKindVideo\n\t}\n\tif fault := captionFault(m.caption); fault != \"\" {\n\t\treturn fault\n\t}\n\tif len(m.mirrors) == 0 {\n\t\treturn \"a media item needs somewhere to find it\"\n\t}\n\tif len(m.mirrors) \u003e maxMirrorsPerItem {\n\t\treturn \"a media item lists at most \" + strconv.Itoa(maxMirrorsPerItem) + \" mirrors\"\n\t}\n\tfor _, u := range m.mirrors {\n\t\tif fault := mirrorFault(u); fault != \"\" {\n\t\t\treturn fault\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// parseMediaArg reads the argument a caller hands OpenClaimPM.\n//\n// The format is deliberately dull, because parsing is the expensive direction\n// and no JSON package is in this realm's dependency set:\n//\n//\tkind|sha256|mime|w|h|bytes|caption|mirror[,mirror...]\n//\n// one item per line. Newlines are fine HERE — this is a transaction argument.\n// They are not fine on the way out (see encodeMedia).\n//\n// Every fault panics: a claim is filed once and forever, so a media list that\n// was silently dropped would be discovered by the author only after they had\n// paid the deposit for a claim they cannot amend.\n// parseMediaFault is the fault half, split from the panic for the reason\n// siteDomainFault is: a validator that can only abort cannot be asked a question,\n// and every arm of it would then need a court fixture and a crossing call to\n// test. The realm's own idiom is fault-then-must, and this follows it.\nfunc parseMediaFault(arg string) ([]mediaItem, string) {\n\ttrimmed := strings.TrimSpace(arg)\n\tif trimmed == \"\" {\n\t\treturn nil, \"\"\n\t}\n\tlines := strings.Split(trimmed, \"\\n\")\n\tif len(lines) \u003e maxClaimMediaCount {\n\t\treturn nil, \"a claim carries at most \" +\n\t\t\tstrconv.Itoa(maxClaimMediaCount) + \" media items\"\n\t}\n\tout := make([]mediaItem, 0, len(lines))\n\tfor _, line := range lines {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tf := strings.Split(line, \"|\")\n\t\tif len(f) != 8 {\n\t\t\treturn nil, \"a media item has 8 fields separated by |\"\n\t\t}\n\t\tm := mediaItem{\n\t\t\tkind:    f[0],\n\t\t\tsha256:  f[1],\n\t\t\tmime:    f[2],\n\t\t\tw:       atoiOr0(f[3]),\n\t\t\th:       atoiOr0(f[4]),\n\t\t\tbytes:   atoiOr0(f[5]),\n\t\t\tcaption: f[6],\n\t\t}\n\t\tfor _, u := range strings.Split(f[7], \",\") {\n\t\t\tif u != \"\" {\n\t\t\t\tm.mirrors = append(m.mirrors, u)\n\t\t\t}\n\t\t}\n\t\tif fault := mediaItemFault(\u0026m); fault != \"\" {\n\t\t\treturn nil, fault\n\t\t}\n\t\tout = append(out, m)\n\t}\n\treturn out, \"\"\n}\n\nfunc parseMediaArg(arg string) []mediaItem {\n\titems, fault := parseMediaFault(arg)\n\tif fault != \"\" {\n\t\tpanic(\"kourtv2: \" + fault)\n\t}\n\treturn items\n}\n\nfunc atoiOr0(s string) int {\n\tn, err := strconv.Atoi(s)\n\tif err != nil {\n\t\treturn 0\n\t}\n\treturn n\n}\n\n// jsonString quotes a Go string for the JSON emitted by encodeMedia. Only `\"`\n// and `\\` and the control characters need handling, and captionFault has already\n// refused the controls — but this escapes them anyway rather than trusting a\n// check made somewhere else about a value read back out of storage.\nfunc jsonString(s string) string {\n\tvar b strings.Builder\n\tb.WriteString(`\"`)\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tc := s[i]\n\t\tswitch c {\n\t\tcase '\"':\n\t\t\tb.WriteString(`\\\"`)\n\t\tcase '\\\\':\n\t\t\tb.WriteString(`\\\\`)\n\t\tcase '\\n':\n\t\t\tb.WriteString(`\\n`)\n\t\tcase '\\r':\n\t\t\tb.WriteString(`\\r`)\n\t\tcase '\\t':\n\t\t\tb.WriteString(`\\t`)\n\t\tdefault:\n\t\t\tif c \u003c 0x20 {\n\t\t\t\tb.WriteString(`\\u00`)\n\t\t\t\tconst hex = \"0123456789abcdef\"\n\t\t\t\tb.WriteByte(hex[c\u003e\u003e4])\n\t\t\t\tb.WriteByte(hex[c\u00260xf])\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb.WriteByte(c)\n\t\t}\n\t}\n\tb.WriteString(`\"`)\n\treturn b.String()\n}\n\n// encodeMedia is the READ format, and it is JSON on ONE LINE for a reason that\n// is not taste. The overlay's qeval parser splits raw output on newlines BEFORE\n// it matches a typed value, so any newline in this payload is read as a second,\n// malformed value. JSON also carries captions containing spaces, several\n// mirrors, and the purge tombstone without inventing a second separator scheme.\n//\n// Emitting JSON is concatenation; parsing it would need a package this realm\n// does not depend on. The wire format is asymmetric so each direction does the\n// cheap thing.\nfunc encodeMedia(items []mediaItem) string {\n\tif len(items) == 0 {\n\t\treturn \"[]\"\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"[\")\n\tfor i := range items {\n\t\tif i \u003e 0 {\n\t\t\tb.WriteString(\",\")\n\t\t}\n\t\tm := \u0026items[i]\n\t\tb.WriteString(`{\"kind\":`)\n\t\tb.WriteString(jsonString(m.kind))\n\t\t// A purged slot keeps its POSITION and loses everything else. The reader\n\t\t// is told there was something here and that it was taken down, which is\n\t\t// honest, and the indices stay the ones a moderator's next call will use.\n\t\tif m.purged {\n\t\t\tb.WriteString(`,\"purged\":true}`)\n\t\t\tcontinue\n\t\t}\n\t\tb.WriteString(`,\"sha256\":`)\n\t\tb.WriteString(jsonString(m.sha256))\n\t\tb.WriteString(`,\"mime\":`)\n\t\tb.WriteString(jsonString(m.mime))\n\t\tb.WriteString(`,\"w\":`)\n\t\tb.WriteString(strconv.Itoa(m.w))\n\t\tb.WriteString(`,\"h\":`)\n\t\tb.WriteString(strconv.Itoa(m.h))\n\t\tb.WriteString(`,\"bytes\":`)\n\t\tb.WriteString(strconv.Itoa(m.bytes))\n\t\tb.WriteString(`,\"caption\":`)\n\t\tb.WriteString(jsonString(m.caption))\n\t\tb.WriteString(`,\"mirrors\":[`)\n\t\tn := 0\n\t\tfor _, u := range m.mirrors {\n\t\t\t// Re-validated on the way OUT, not only on the way in. Write-time\n\t\t\t// validation alone grandfathers every mirror stored under an older,\n\t\t\t// wider allowlist — so retiring a host would fail to retract the\n\t\t\t// images already pointing at it, which would make the allowlist a\n\t\t\t// formality rather than a control.\n\t\t\tif mirrorFault(u) != \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif n \u003e 0 {\n\t\t\t\tb.WriteString(\",\")\n\t\t\t}\n\t\t\tb.WriteString(jsonString(u))\n\t\t\tn++\n\t\t}\n\t\tb.WriteString(`]}`)\n\t}\n\tb.WriteString(\"]\")\n\treturn b.String()\n}\n"},{"name":"meta.gno","body":"// The meta court: the appeals layer (MODERATION.md §3.3).\n//\n// It is not a special authority bolted onto moderation — it IS a court,\n// deployed at realm init under the reserved slug \"meta\", and it decides appeals\n// through the same bonded machinery every other claim uses: answer bond → 72h →\n// optional dispute → sealed vote → verdict. What makes a claim an appeal is\n// only its title: a reserved `mod:` schema naming a verb and a target. When\n// such a claim reaches a final YES verdict, anyone may execute it, and the\n// execution flips a render-layer bit (or replaces a moderator set).\n//\n// Everything here is bounded by the constitution: an execution can hide, unhide,\n// suspend, or reseat — it can never move a coin. A captured meta court censors\n// discovery and nothing else, and the global DAO can undo every one of its acts\n// (ClearAnyBit, ClearCourtSuspension, ResetModSet).\n//\n// The guard set is the expensive part. Three independent adversarial vets shaped\n// it, and every guard below exists because a specific attack got through\n// without it — the comments name them.\n\npackage kourtv2\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"strings\"\n\n\tunsafe \"chain/runtime/unsafe\"\n\n\t\"math/overflow\"\n\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\n// realmPath is this realm's own package path — the same one buy.gno derives its\n// burn sink from. init cannot ask `cur` for the realm address, so the escrow\n// address is derived from the path instead.\nconst metaRealmPath = \"gno.land/r/kourt/kourtv2\"\n\nconst (\n\tmetaPrefix   = \"mod:\"\n\tmetaCourtNm  = \"Meta\"\n\tmetaExecuted = \"MetaExecuted\"\n)\n\n// The verbs. Restorative verbs may execute from the undisputed (silence) route:\n// silence may RESTORE visibility. Aggressive verbs may not — silence must never\n// remove visibility, disarm a court, re-arm one, or reseat its moderators.\n//\n// AND THESE STRINGS ARE THE PROTOCOL. A verb is not an identifier the compiler\n// checks. It is the middle of a claim's TITLE, typed by whoever files the appeal\n// and matched here when it is read — syntax and code at the same time. So a verb\n// is renamed the way a wire format is, not the way a symbol is: every title\n// already written with the other word stops parsing as an appeal, and only the\n// structured parse stored at open still knows what it meant.\nconst (\n\tverbUnhide    = \"unhide\"    // restorative: clear the courtMod bit\n\tverbClear     = \"clear\"     // restorative: clear the meta bit\n\tverbHide      = \"hide\"      // aggressive: set the meta bit\n\tverbSuspend   = \"suspend\"   // aggressive: disarm a moderator set\n\tverbUnsuspend = \"unsuspend\" // aggressive: re-arm one (it re-applies masked hides)\n\tverbSetmods   = \"setmods\"   // aggressive: install a moderator set\n\t// RESTORATIVE, and the classification is the whole design decision. An\n\t// aggressive verb may execute only from a DECIDED CONTESTED vote with weight\n\t// against it — right for seizing authority, perverse for a set: an\n\t// uncontested, obviously-good proposal could never carry, so the verb would\n\t// be unusable in exactly the case it exists for. A set is additive, has zero\n\t// economic weight, and one moderator undoes it with RetireFolder — the same\n\t// reasoning folders.gno already uses to keep set ops off the m-of-n\n\t// ceremony. The undisputed route is not a free pass: it still needs a bonded\n\t// YES answer and 72h in which any holder may dispute.\n\tverbNewset = \"newset\" // restorative: create a set the court agreed to\n)\n\nfunc verbIsAggressive(v string) bool {\n\treturn v == verbHide || v == verbSuspend || v == verbUnsuspend || v == verbSetmods\n}\n\n// modParse is a claim title parsed into a binding instruction. It is STORED\n// STRUCTURED STATE, not re-derived at read time: a purged title is unrecoverable\n// text, so a render-time re-parse would both blind the appeal's own electorate\n// and orphan its latch.\ntype modParse struct {\n\tvalid     bool\n\tverb      string\n\tcourt     string\n\ttarget    uint64 // claimID for item verbs, candidateID for setmods\n\thasTarget bool\n\tpoisoned  bool // purged before the binding was persisted: render-only\n}\n\n// metaState holds the meta court's per-claim parses, latches, and execution\n// flags. Keyed by the meta court's own claim ids — it is deliberately NOT a\n// field on the shared claimState, which every court's claims carry.\ntype metaState struct {\n\tparses   *bptree.BPTree // claimID -\u003e *modParse (stored at open, rewritten on edit)\n\tbindings *bptree.BPTree // claimID -\u003e *modParse (persisted at PostAnswer; the only thing execution reads)\n\tlatches  *bptree.BPTree // latchKey -\u003e claimID\n\texecuted *bptree.BPTree // claimID -\u003e true (exactly-once)\n}\n\nvar meta = \u0026metaState{\n\tparses:   bptree.NewBPTree32(),\n\tbindings: bptree.NewBPTree32(),\n\tlatches:  bptree.NewBPTree32(),\n\texecuted: bptree.NewBPTree32(),\n}\n\n// init deploys the meta court. init has no `cur` to ask, and MsgAddPackage sets\n// OriginCaller to the deployer — the r/govern/token.gno pattern, and the only\n// moment the deployer can be read without a caller influencing it. It calls\n// startCourt directly rather than StartCourt, because the public path reserves\n// this slug precisely so only init can take it.\nfunc init() {\n\tstartCourt(unsafe.OriginCaller(), chain.PackageAddress(metaRealmPath), metaSlug, metaCourtNm, 0)\n}\n\n// parseModTitle reads a reserved title. Anything that is not an exact match is\n// simply an ordinary meta-court claim (render badges it \"not a valid appeal\", so\n// a near-miss cannot masquerade as binding).\n//\n//\tmod:unhide:\u003ccourt\u003e/\u003cclaimID\u003e   mod:clear:\u003ccourt\u003e/\u003cclaimID\u003e\n//\tmod:hide:\u003ccourt\u003e/\u003cclaimID\u003e     mod:setmods:\u003ccourt\u003e/\u003ccandidateID\u003e\n//\tmod:suspend:\u003ccourt\u003e            mod:unsuspend:\u003ccourt\u003e\n//\tmod:newset:\u003ccourt\u003e/\u003cfolderProposalID\u003e\nfunc parseModTitle(title string) modParse {\n\tp := modParse{}\n\tif !strings.HasPrefix(title, metaPrefix) {\n\t\treturn p\n\t}\n\trest := title[len(metaPrefix):]\n\tslash := strings.Index(rest, \":\")\n\tif slash \u003c 0 {\n\t\treturn p\n\t}\n\tverb := rest[:slash]\n\targ := rest[slash+1:]\n\tswitch verb {\n\tcase verbUnhide, verbClear, verbHide, verbSetmods, verbNewset:\n\t\tcut := strings.Index(arg, \"/\")\n\t\tif cut \u003c= 0 || cut == len(arg)-1 {\n\t\t\treturn p\n\t\t}\n\t\tid, err := strconv.ParseUint(arg[cut+1:], 10, 64)\n\t\tif err != nil || id == 0 {\n\t\t\treturn p\n\t\t}\n\t\tp.court, p.target, p.hasTarget = arg[:cut], id, true\n\tcase verbSuspend, verbUnsuspend:\n\t\tif arg == \"\" || strings.Contains(arg, \"/\") {\n\t\t\treturn p\n\t\t}\n\t\tp.court = arg\n\tdefault:\n\t\treturn p\n\t}\n\t// The slug charset is [a-z0-9-], so a malformed target cannot hide in it.\n\tfor i := 0; i \u003c len(p.court); i++ {\n\t\tch := p.court[i]\n\t\tif !((ch \u003e= 'a' \u0026\u0026 ch \u003c= 'z') || (ch \u003e= '0' \u0026\u0026 ch \u003c= '9') || ch == '-') {\n\t\t\treturn modParse{}\n\t\t}\n\t}\n\tp.verb, p.valid = verb, true\n\treturn p\n}\n\n// latchKey serialises appeals so one target cannot host a swarm of them.\n// setmods is keyed per COURT, not per candidate: a per-candidate key would let\n// an attacker bank unlimited parallel installs on one court and discharge them\n// one after each local election.\nfunc latchKey(p modParse) string {\n\tswitch p.verb {\n\tcase verbSetmods:\n\t\treturn \"set:\" + p.court\n\t// Per COURT, for setmods' reason: keyed per proposal, an attacker could bank\n\t// unlimited parallel folder installs and discharge them one after another.\n\tcase verbNewset:\n\t\treturn \"fold:\" + p.court\n\tcase verbSuspend, verbUnsuspend:\n\t\treturn \"sus:\" + p.court\n\tdefault:\n\t\treturn \"item:\" + p.court + \":\" + strconv.FormatUint(p.target, 10)\n\t}\n}\n\n// ---- lifecycle hooks (called from the claim/answer paths) -----------------\n\n// onClaimOpened stores the parse for a meta-court claim, and puts a valid\n// unanswered appeal on the pending list so it is force-visible from birth (an\n// appeal nobody can find is an appeal nobody can contest).\nfunc onClaimOpened(c *Court, cs *claimState) {\n\tif c.id != metaSlug {\n\t\treturn\n\t}\n\tp := parseModTitle(cs.title)\n\tmeta.parses.Set(beClaimKey(cs.id), \u0026p)\n\tcs.noAccuracy = p.valid \u0026\u0026 p.verb == verbNewset\n\tif p.valid {\n\t\tpendingEnter(c, cs)\n\t}\n}\n\n// onTitleEdited re-parses a meta claim whose title changed inside the polish\n// window. It deliberately performs NO latch work: edits are provably pre-answer\n// (the window closes before a claim can mature enough to answer) and the latch\n// binds at PostAnswer, so an edit-time acquire would reopen the camp it closed.\nfunc onTitleEdited(c *Court, cs *claimState) {\n\tif c.id != metaSlug {\n\t\treturn\n\t}\n\tp := parseModTitle(cs.title)\n\t// Carry the purge poison forward: re-parsing must not re-arm an appeal whose\n\t// title was purged during the polish window.\n\tif prev := meta.parses.Get(beClaimKey(cs.id)); prev != nil \u0026\u0026 prev.(*modParse).poisoned {\n\t\tp.poisoned = true\n\t}\n\tmeta.parses.Set(beClaimKey(cs.id), \u0026p)\n\t// AND THE PAY LANE FOLLOWS THE TITLE. noAccuracy is decided by what the title\n\t// says, so an edit inside the polish window has to re-decide it: a claim\n\t// edited OUT of the newset schema must pay its stakers like any other, and\n\t// one edited INTO it must not. The window closes before a claim can be\n\t// answered, so nothing has been staked on the old answer by now.\n\tcs.noAccuracy = p.valid \u0026\u0026 p.verb == verbNewset\n\t// A SEEDED claim stays on the pending list whatever its title says — the set\n\t// that seeds content must never be able to embargo it by editing the title\n\t// into a non-appeal string (M-A18).\n\tif p.valid || cs.seeded {\n\t\tpendingEnter(c, cs)\n\t} else {\n\t\tpendingExit(c, cs)\n\t}\n}\n\n// onClaimAnswered persists the BINDING parse and takes the target's latch.\n// First-to-answer takes it, which forces anyone who wants to hold a target's\n// appeal lane onto the bonded, disputable path rather than a 0.1-CC claim that\n// is never answered.\nfunc onClaimAnswered(c *Court, cs *claimState) {\n\tif c.id != metaSlug {\n\t\treturn\n\t}\n\tv := meta.parses.Get(beClaimKey(cs.id))\n\tif v == nil {\n\t\treturn\n\t}\n\tp := v.(*modParse)\n\tif !p.valid || p.poisoned {\n\t\treturn // an ordinary claim, or a purged-before-binding one: never executable\n\t}\n\tk := latchKey(*p)\n\tif held := meta.latches.Get(k); held != nil {\n\t\tif held.(uint64) != cs.id {\n\t\t\tpanic(\"kourtv2: another appeal already holds this target's review slot\")\n\t\t}\n\t}\n\tmeta.latches.Set(k, cs.id)\n\tbind := *p\n\tmeta.bindings.Set(beClaimKey(cs.id), \u0026bind)\n}\n\n// releaseMetaLatchIfMeta frees a meta appeal's target slot at any TERMINAL\n// transition of the appeal claim. It exists because the latch used to be freed\n// only on a SUCCESSFUL execution — so every appeal that was refused, expired, or\n// simply voted down held its target's review slot FOREVER, permanently bricking\n// that target's appeals lane and permanently reverting PostAnswer for it. That\n// fires on the ordinary path, not just under attack: a hide appeal correctly\n// refused on silence would have poisoned the item for good. Non-panicking, so\n// it is safe on a money path.\nfunc releaseMetaLatchIfMeta(c *Court, cs *claimState) {\n\tif c.id != metaSlug {\n\t\treturn\n\t}\n\treleaseMetaLatch(cs)\n}\n\n// releaseMetaLatch frees a target's slot once its appeal can no longer execute.\nfunc releaseMetaLatch(cs *claimState) {\n\tv := meta.bindings.Get(beClaimKey(cs.id))\n\tif v == nil {\n\t\treturn\n\t}\n\tk := latchKey(*v.(*modParse))\n\tif held := meta.latches.Get(k); held != nil \u0026\u0026 held.(uint64) == cs.id {\n\t\tmeta.latches.Remove(k)\n\t}\n}\n\n// poisonMetaParse marks a purged appeal's parse render-only. The parse still\n// RENDERS (verb+target are realm-validated strings, the same speech class as a\n// category code, so redacting them would blind the appeal's electorate for no\n// legal-hold gain) but it can no longer bind an execution.\nfunc poisonMetaParse(claimID uint64) {\n\tif v := meta.parses.Get(beClaimKey(claimID)); v != nil {\n\t\tv.(*modParse).poisoned = true\n\t}\n}\n\n// ---- execution ------------------------------------------------------------\n\n// metaVerdictExpired reports whether a decided appeal has outlived its execution\n// window. Pure but for the clock, and a function rather than four inline lines\n// because the fixture that reaches ExecuteMetaVerdict is expensive — a court, a\n// hidden target, a matured ring, an answer and a settlement — and the edge below\n// is worth pinning directly rather than through all of that.\n//\n// THE WINDOW IS ONE VOTE LONG, so it is measured in the unit the vote was\n// promised in; votingBlocks is a court parameter in blocks and converts here.\n//\n// THE `+1` IS THE INCLUSIVE EDGE, NOT A LONGER WINDOW. The height form expires\n// on `\u003e`, so executing exactly AT verdictAt+votingBlocks has always been allowed;\n// pastDeadline answers `\u003e=`, so the seconds arm asks for one unit more to keep\n// that last instant. Dropping it shortens every appeal window by a second, and\n// nothing would have said so: mutating either edge SURVIVED the whole suite\n// before this was pinned.\nfunc metaVerdictExpired(cs *claimState, votingBlocks int64) bool {\n\tpassed, known := pastDeadline(cs.verdictAtTime, blocksToSecs(votingBlocks)+1)\n\treturn (known \u0026\u0026 passed) || (!known \u0026\u0026 heightNow() \u003e cs.verdictAt+votingBlocks)\n}\n\n// ExecuteMetaVerdict applies a decided appeal. Permissionless: the verdict is\n// the authority, not the caller.\n//\n// The guard order is FIXED and validity comes before state, so a call that\n// should do nothing can never leave a mark (an earlier ordering let a non-final\n// call stamp the re-hide cooldown).\nfunc ExecuteMetaVerdict(cur realm, metaClaimID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tmc := mustCourt(metaSlug)\n\tcs := mustClaim(mc, metaClaimID)\n\n\t// 1. A binding parse must exist. Purged-before-binding executes nothing;\n\t//    a purge AFTER the binding never blocks execution, which is what stops\n\t//    purge from becoming a veto over the appeals layer.\n\tbv := meta.bindings.Get(beClaimKey(metaClaimID))\n\tif bv == nil {\n\t\tpanic(\"kourtv2: this claim carries no binding appeal\")\n\t}\n\tp := *bv.(*modParse)\n\n\t// 2. The target must exist and STRICTLY predate the appeal, so no appeal can\n\t//    be aimed at something created after voters judged it.\n\tif !Exists(p.court) {\n\t\tpanic(\"kourtv2: the target court does not exist\")\n\t}\n\ttc := mustCourt(p.court)\n\tif tc.createdAt \u003e= cs.openedAt {\n\t\tpanic(\"kourtv2: the target court does not predate this appeal\")\n\t}\n\tvar target *claimState\n\t// newset's target is a PROPOSAL, not a claim, so it is excluded here for\n\t// the reason setmods is and gets its own predate check below.\n\tif p.hasTarget \u0026\u0026 p.verb != verbSetmods \u0026\u0026 p.verb != verbNewset {\n\t\ttarget = mustClaim(tc, p.target)\n\t\tif target.openedAt \u003e= cs.openedAt {\n\t\t\tpanic(\"kourtv2: the target claim does not predate this appeal\")\n\t\t}\n\t}\n\tif p.verb == verbSetmods {\n\t\t// The candidate is the target here, and it must ALSO predate the appeal.\n\t\t// Registration is permissionless with a predictable per-court counter, so\n\t\t// without this an attacker opens setmods against an unallocated id, wins\n\t\t// the vote on a candidate nobody could inspect, and only then registers\n\t\t// their own puppet set at that id.\n\t\tif cand := ensureMod(tc).mustCandidate(p.target); cand.at \u003e= cs.openedAt {\n\t\t\tpanic(\"kourtv2: the candidate set does not predate this appeal\")\n\t\t}\n\t}\n\tif p.verb == verbNewset {\n\t\t// Same attack, same shape: registration is permissionless with a\n\t\t// predictable per-court counter, so an appeal aimed at an unallocated\n\t\t// proposal id would let its author win a vote on a folder nobody could\n\t\t// read and register the real payload afterwards.\n\t\tif prop := ensureMod(tc).mustFolderProposal(p.target); prop.at \u003e= cs.openedAt {\n\t\t\tpanic(\"kourtv2: the folder proposal does not predate this appeal\")\n\t\t}\n\t}\n\n\t// 3. A final YES verdict — read from `provisional`, the field a dispute\n\t//    OVERTURNS, never from the posted answer.\n\tif cs.verdictAt == 0 || cs.provClose {\n\t\tpanic(\"kourtv2: no final verdict\")\n\t}\n\tif int(cs.provisional) != sideYES {\n\t\tpanic(\"kourtv2: the verdict was not YES\")\n\t}\n\n\t// 4. Aggressive verbs need a genuinely contested route. `route == \"vote\"`\n\t//    alone is forgeable: it is stamped for ANY once-disputed claim, including\n\t//    one whose only round FAILED QUORUM — so silence plus a self-dispute\n\t//    would seat a principal on someone else's court for ~10% of a\n\t//    self-chosen stake. decidedRounds proves a quorate round classified it.\n\tif verbIsAggressive(p.verb) {\n\t\tif cs.route != \"vote\" || cs.decidedRounds == 0 {\n\t\t\tpanic(\"kourtv2: this verb needs a decided contested vote, not silence\")\n\t\t}\n\t\t// credEligible (dispute.gno) means ≥ floor/4 of weight voted AGAINST the\n\t\t// claim — a real adversary showed up. Without it the surviving forge is\n\t\t// \"hold 5% of meta supply and abstain-vote your own appeal\": abstentions\n\t\t// clear the quorum, yes=0 falls to the uphold default, and decidedRounds\n\t\t// stamps. That buys a contested-looking verdict with ZERO opposing\n\t\t// weight, so the verbs that seize authority demand real opposition.\n\t\t//\n\t\t// hide is deliberately EXEMPT (3-agent panel, 2/3 with the carve-out\n\t\t// reasoned only by B): requiring an adversary would mean a correct but\n\t\t// UNCONTESTED hide could never execute, and hide is the one aggressive\n\t\t// verb that is low-harm and globally reversible — the global DAO clears\n\t\t// it, and it moves no money.\n\t\tif p.verb != verbHide \u0026\u0026 !cs.credEligible {\n\t\t\tpanic(\"kourtv2: a manufactured contest cannot seize authority — this verb needs weight voted against it\")\n\t\t}\n\t}\n\n\t// 5. Exactly-once. Without it, anyone could re-call a settled appeal every\n\t//    votingBlocks-1 and extend the respondent's cooldown forever — the\n\t//    livelock, mirrored onto the moderator side.\n\tif meta.executed.Has(beClaimKey(metaClaimID)) {\n\t\tpanic(\"kourtv2: already executed\")\n\t}\n\n\t// 6. Execution expiry: a verdict is not a sleeper round. Without this a\n\t//    party could bank K decided appeals and discharge one after each local\n\t//    election, inverting last-writer-wins into oldest-ammunition-wins.\n\t//    THE WINDOW IS ONE VOTE LONG, so it is measured in the unit the vote was\n\t//    promised in. mc.params.votingBlocks is a court parameter in blocks and\n\t//    converts at the call site.\n\t//\n\t//    THE `+1` IS THE INCLUSIVE EDGE, NOT A LONGER WINDOW. The height form\n\t//    expires on `\u003e`, so executing exactly AT verdictAt+votingBlocks has always\n\t//    been allowed; pastDeadline answers `\u003e=`, so the seconds arm has to ask\n\t//    for one unit more to keep that last instant. Dropping it would shorten\n\t//    every appeal window by a second on the quiet.\n\tif metaVerdictExpired(cs, mc.params.votingBlocks) {\n\t\tpanic(\"kourtv2: this verdict has expired; re-appeal\")\n\t}\n\n\t// 7. Staleness, per verb. setmods anchors on verdictAt — NOT openedAt: the\n\t//    local electorate is always faster than a meta appeal, so an openedAt\n\t//    anchor would hand it a permanent free veto and meta's duty cycle would\n\t//    be zero. An install between the verdict and its execution refuses.\n\tcm := ensureMod(tc)\n\tswitch p.verb {\n\tcase verbSetmods:\n\t\tif cm.setActHeight \u003e cs.verdictAt {\n\t\t\tpanic(\"kourtv2: the moderator set changed after this verdict; re-appeal\")\n\t\t}\n\t\t// The global DAO's term applies to THIS verb too (v0.55, 3-0). setmods\n\t\t// looks like a membership verb, so this arm was written against\n\t\t// setActHeight alone — but installModSet RE-ARMS a suspended court, the\n\t\t// same `cm.suspended = false` unsuspendSet performs, and GlobalSuspendSet\n\t\t// stamps suspendActByGlobal WITHOUT moving setActHeight. So a stale\n\t\t// verdict was invisible on this path: `setmods` was a cheaper `unsuspend`\n\t\t// than `unsuspend`, qualified only by its spelling, and a registered\n\t\t// candidate is free and permissionless.\n\t\t//\n\t\t// Anchored on openedAt, unlike the setActHeight term above. That term's\n\t\t// verdictAt anchor is about the LOCAL electorate, which moves on a cadence\n\t\t// fast enough to veto meta permanently; the global DAO's is m-of-n and\n\t\t// GlobalSuspendSet REFUSES while already suspended, so it cannot re-arm\n\t\t// the veto without first clearing the suspension the appellant wants gone.\n\t\t// The cost is one re-appeal per global act, exactly what the arm below\n\t\t// already prices in. An asymmetric anchor between two arms of one switch\n\t\t// is what produced this bug.\n\t\t//\n\t\t// Direction-scoping needs no second clause: installModSet can only ever\n\t\t// CLEAR the flag, so `cm.suspended` IS the opposite-direction test. A\n\t\t// global ClearCourtSuspension also stamps suspendActByGlobal but leaves\n\t\t// the flag false, so a legitimate install still proceeds.\n\t\tif cm.suspended \u0026\u0026 cm.suspendActByGlobal \u003e cs.openedAt {\n\t\t\tpanic(\"kourtv2: the global DAO changed this court's suspension under this appeal; re-appeal\")\n\t\t}\n\tcase verbSuspend, verbUnsuspend:\n\t\tif cm.lastElectionAt \u003e cs.openedAt || cm.setActHeight \u003e cs.verdictAt {\n\t\t\tpanic(\"kourtv2: the moderator set changed under this appeal; re-appeal\")\n\t\t}\n\t\t// The global DAO's own term. Its suspension verbs are the legal\n\t\t// backstop, so a meta verdict that predates a global decision must not\n\t\t// silently overturn it — a stale `suspend` could re-suspend a court the\n\t\t// global DAO just cleared, and a stale `unsuspend` could release one it\n\t\t// just suspended. Only OPPOSITE-direction acts refuse, which is the same\n\t\t// rule verbUnhide settled on: refusing on a same-direction act buys\n\t\t// nothing (the verdict's goal is already achieved) and costs a re-appeal.\n\t\tif cm.suspendActByGlobal \u003e cs.openedAt \u0026\u0026\n\t\t\tcm.suspended == (p.verb == verbUnsuspend) {\n\t\t\tpanic(\"kourtv2: the global DAO changed this court's suspension under this appeal; re-appeal\")\n\t\t}\n\tcase verbUnhide:\n\t\t// Only an ELECTION refuses an unhide. A third-party CLEAR is\n\t\t// same-direction as the verdict, and refusing on it once let a\n\t\t// moderator re-hide for free after a global clear.\n\t\tif cm.lastElectionAt \u003e cs.openedAt {\n\t\t\tpanic(\"kourtv2: the moderator set changed under this appeal; re-appeal\")\n\t\t}\n\t}\n\n\tmeta.executed.Set(beClaimKey(metaClaimID), true)\n\tapplyMetaVerb(mc, tc, cm, p, cs, target)\n\treleaseMetaLatch(cs)\n\tchain.Emit(metaExecuted,\n\t\t\"appeal\", strconv.FormatUint(metaClaimID, 10),\n\t\t\"verb\", p.verb, \"court\", p.court,\n\t\t\"target\", strconv.FormatUint(p.target, 10),\n\t)\n}\n\nfunc applyMetaVerb(mc, tc *Court, cm *courtMod, p modParse, cs, target *claimState) {\n\tby := cs.answerer\n\tswitch p.verb {\n\tcase verbUnhide:\n\t\t// A no-op unhide (the bit is already clear) stamps NOTHING: otherwise a\n\t\t// chain of appeals against an always-visible item would hold a\n\t\t// moderator's hide power off pre-emptively.\n\t\tclm := lookupClaimMod(cm, p.target)\n\t\tif clm == nil || !clm.court {\n\t\t\treturn\n\t\t}\n\t\tclearCourtBitByMeta(tc, p.target, by)\n\tcase verbClear:\n\t\tclearMetaBit(tc, p.target, by)\n\tcase verbHide:\n\t\tsetMetaBit(tc, p.target, by)\n\tcase verbSuspend:\n\t\tsuspendSet(tc, by)\n\tcase verbUnsuspend:\n\t\tunsuspendSet(tc, by)\n\tcase verbNewset:\n\t\t/* RE-CHECKED AT EXECUTION, because weeks pass between a proposal and the\n\t\t   verdict that carries it: the court can fill up, the name can be taken\n\t\t   by an ordinary CreateFolder, and the parent can be struck from the tree.\n\t\t   A name that has since been taken is a REFUSAL, not a duplicate.\n\t\t   THIS COMMENT USED TO SAY \"every rule newFolderCore enforces is re-run\n\t\t   here\", and listed the retired parent among them. Two of the three are\n\t\t   its rules — the cap and the depth. Liveness is NOT: newFolderCore and\n\t\t   mustNestable contain no reference to `retired` or `purged` at all, so\n\t\t   the case the sentence named as covered was the one thing nothing\n\t\t   checked. */\n\t\tprop := cm.mustFolderProposal(p.target)\n\t\tif folderNameTaken(cm, prop.name) {\n\t\t\tpanic(\"kourtv2: a folder by that name already exists\")\n\t\t}\n\t\t/* A PARENT THAT LEFT THE TREE IS A REFUSAL. Without this a carried verdict\n\t\t   builds a LIVE folder under a struck one — the shape RetireFolder refuses\n\t\t   to create (\"retire the folders inside it first, so the tree cannot grow\n\t\t   orphans\"), reached from underneath instead of from above. Same hole\n\t\t   New had, and the same fix, one verb over.\n\t\t   PURGED AS WELL AS RETIRED, which is where this differs from New's\n\t\t   guard. That one reads its parent from ClaimFolders, and ClaimFolders\n\t\t   already skips purged rows, so a purged parent cannot reach it and a\n\t\t   branch for one would be dead. This parent comes from a proposal STORED\n\t\t   WEEKS AGO, which nothing revisits when the folder is purged — and a\n\t\t   purged parent is the worse of the two, since PurgeFolder erases the name\n\t\t   and leaves the children.\n\t\t   NOT AT REGISTRATION INSTEAD: the folder is live when the proposal is\n\t\t   written and struck later, which is exactly the drift the block above\n\t\t   exists for. Refusing here costs a re-file; refusing there would not have\n\t\t   seen it. */\n\t\tif prop.parent != 0 {\n\t\t\tif pf := cm.mustFolder(prop.parent); pf.retired || pf.purged {\n\t\t\t\tpanic(\"kourtv2: the parent heading is no longer in the tree\")\n\t\t\t}\n\t\t}\n\t\tfid := newFolderCore(cm, prop.name, prop.desc, prop.parent)\n\t\temitModAct(tc.id, 0, \"folder-create\", by)\n\t\tchain.Emit(\"FolderCreated\",\n\t\t\t\"court\", tc.id, \"by\", \"meta\",\n\t\t\t\"folder\", strconv.FormatUint(fid, 10),\n\t\t\t\"proposal\", strconv.FormatUint(p.target, 10),\n\t\t\t\"height\", eventHeight(),\n\t\t)\n\tcase verbSetmods:\n\t\tcand := cm.mustCandidate(p.target)\n\t\tinstallModSet(tc, cm, cand, heightNow(), false)\n\t\tchain.Emit(\"ModSetInstalled\",\n\t\t\t\"court\", tc.id, \"by\", \"meta\",\n\t\t\t\"candidate\", strconv.FormatUint(cand.id, 10),\n\t\t\t\"height\", eventHeight(),\n\t\t)\n\t}\n}\n\n// ---- reads ----------------------------------------------------------------\n\n// AppealParse exposes a meta claim's parsed instruction for render: the verb and\n// target of a valid appeal, or \"\" when the title is not a reserved schema.\nfunc AppealParse(claimID uint64) (verb, court string, target uint64, valid bool) {\n\tv := meta.parses.Get(beClaimKey(claimID))\n\tif v == nil {\n\t\treturn \"\", \"\", 0, false\n\t}\n\tp := v.(*modParse)\n\treturn p.verb, p.court, p.target, p.valid\n}\n\n// AppealExecuted reports whether an appeal has already been applied.\nfunc AppealExecuted(claimID uint64) bool { return meta.executed.Has(beClaimKey(claimID)) }\n\n// AppealLatchHolder is the claim currently holding a target's review slot.\nfunc AppealLatchHolder(verb, court string, target uint64) uint64 {\n\tp := modParse{verb: verb, court: court, target: target}\n\tif v := meta.latches.Get(latchKey(p)); v != nil {\n\t\treturn v.(uint64)\n\t}\n\treturn 0\n}\n\n// ---- the meta franchise (MODERATION.md §13.8) -----------------------------\n//\n// The appeals layer's electorate should be the platform's paying participants,\n// not whoever chose to buy the appeals coin. Selling meta-CC on its own curve\n// makes the security parameter a CONSTANT — one actor's one-time purchase — so\n// a 5% pivot costs ~0.108x the deployer's genesis spend forever, at any\n// platform size. Deriving it from burned GNOT instead makes it ENDOGENOUS: it\n// grows with adoption, and burned GNOT is the one signal nobody can fake.\n//\n// Three independent vets converged on the same shape, and every clause below\n// exists because a specific attack needs it:\n//\n//   - accrue on `spent` µGNOT, NEVER on CC minted. A fresh curve sells CC\n//     ~44,721x cheaper per GNOT and StartCourt is free, so a per-CC rule is an\n//     unbounded sqrt(N) free-mint of the platform's governance token. Per-GNOT\n//     makes the court you burn into irrelevant — and that is sound ONLY because\n//     the GNOT is truly burned to a keyless sink with no redemption path\n//     anywhere. TRIPWIRE: this breaks the instant a treasury or redemption is\n//     reintroduced.\n//   - CLAIM-ON-DEMAND, not an airdrop. Burning accrues an entitlement; meta-CC\n//     exists only when its owner asks for it. This one choice does four jobs:\n//     supply stays equal to the ENGAGED electorate (so the 5%-of-supply quorum\n//     floor stays reachable — an auto-mint would inflate the bar ~158x while\n//     turnout stayed flat, killing honest appeals while the zero-voter forge\n//     kept working); the cheap-float capture route disappears (an attacker must\n//     pay people to claim AND sell, which is visible vote-buying); the\n//     unsolicited distribution becomes an opt-in membership act; and the\n//     unclaimed overhang is a standing DEFENSIVE RESERVE — dormant burners can\n//     claim and dilute a capture next cycle.\n//   - route the mint through META'S OWN CURVE, advancing the same monotone\n//     position its Buy uses. That keeps the quadratic escalator (capture gets\n//     harder as the platform grows), closes the side door where a parked curve\n//     would sell a capture bloc at genesis prices, and leaves court.gno's\n//     supply-ceiling invariant true UNCHANGED — the curve's own cap still\n//     bounds every unit of meta primary issuance.\n//   - CLAMP, never panic. This mint is reached from Buy, the platform's only\n//     GNOT on-ramp; a MaxSupply panic here would brick buying on every court.\n\n// franchise: address -\u003e unclaimed µGNOT burned across all courts.\nvar franchise = bptree.NewBPTree32()\n\n// accrueFranchise records a burn toward the claimant's future meta weight.\n// Called from Buy; never panics, never mints, never reads money state back.\n// Buying META itself does not accrue — that GNOT already bought meta-CC\n// directly through the same curve, and counting it twice would pay the same\n// burn two ways.\nfunc accrueFranchise(c *Court, who address, spent int64) {\n\tif c.id == metaSlug || spent \u003c= 0 {\n\t\treturn\n\t}\n\tk := who.String()\n\tprev := int64(0)\n\tif v := franchise.Get(k); v != nil {\n\t\tprev = v.(int64)\n\t}\n\tsum, ok := overflow.Add64(prev, spent)\n\tif !ok {\n\t\treturn // unreachable at real volumes; never panic on the Buy path\n\t}\n\tfranchise.Set(k, sum)\n}\n\n// ClaimMetaFranchise mints the caller's accrued appeals weight. Permissionless\n// and idempotent — claiming twice with nothing accrued is a no-op returning 0.\nfunc ClaimMetaFranchise(cur realm) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tk := who.String()\n\tv := franchise.Get(k)\n\tif v == nil {\n\t\treturn 0\n\t}\n\towed := v.(int64)\n\tif owed \u003c= 0 {\n\t\treturn 0\n\t}\n\tmc := mustCourt(metaSlug)\n\ttouch(mc) // the mint changes meta's live supply; its accrual clock must be current\n\tdelta, used := mc.crv.Minted(mc.minted, owed)\n\tif delta \u003c= 0 {\n\t\treturn 0 // the curve is at its cap: clamp silently, keep the entitlement\n\t}\n\tmc.minted += delta // ONE monotone position, shared with meta's own Buy\n\trecordPrice(mc)    // a franchise mint moves the curve too\n\tmc.coin.Mint(who, delta)\n\tif used \u003e= owed {\n\t\tfranchise.Remove(k)\n\t} else {\n\t\tfranchise.Set(k, owed-used) // a partial fill leaves the rest claimable\n\t}\n\tchain.Emit(\"FranchiseClaimed\",\n\t\t\"who\", who.String(),\n\t\t\"burned\", strconv.FormatInt(used, 10),\n\t\t\"minted\", strconv.FormatInt(delta, 10),\n\t)\n\treturn delta\n}\n\n// FranchiseOf is the caller-visible unclaimed entitlement, in µGNOT burned.\nfunc FranchiseOf(who address) int64 {\n\tif v := franchise.Get(who.String()); v != nil {\n\t\treturn v.(int64)\n\t}\n\treturn 0\n}\n"},{"name":"moderation.gno","body":"// Moderation: render-layer authority only. No money path reads any state in\n// this file, and no entrypoint here writes coin/escrow/emission/verdict/stake\n// state (MODERATION.md §2, the constitution). Moderation controls DISCOVERY —\n// what appears in listings and on the front page — never money. A hidden claim's\n// full lifecycle (stake, answer, vote, settle, withdraw) runs byte-identically;\n// hiding it only removes it from browse surfaces (it stays reachable by direct\n// link). This is what makes even a fully captured moderator harmless to funds.\n//\n// This file is module 1: the hide-bit model, per-court moderator sets with an\n// m-of-n decision rule, suspension, the global moderator DAO, and purge (the one\n// power that removes text, reserved for legal compliance). Strips (module 3/6),\n// folders and the election (modvote), the meta court's ExecuteMetaVerdict\n// (module 7), and render (module 6) land in sibling files and call the internal\n// helpers here.\n\npackage kourtv2\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\nconst (\n\tmaxReasonLen = 200\n\t// maxModSetSize bounds currentSetID's O(n²) concatenation, which sits on the\n\t// suspend path — see canonicalMembers. 32 is far above any real m-of-n.\n\tmaxModSetSize = 32\n\t// The re-set window mirrors the DMCA §512(g) counter-notice period (~2\n\t// weeks): re-setting a global redaction that a global act cleared inside\n\t// this window needs m-of-n (MODERATION.md §3.2, round-4 F5). Outside it a\n\t// fresh notice is a fresh single-key hide.\n\t//\n\t// A STATUTORY PERIOD IS IN DAYS, so the gates read it in seconds, through\n\t// blocksToSecs at the call site. It stays declared in blocks and there is no\n\t// seconds twin on purpose: counsel must be able to retune this alone, and two\n\t// numbers for one statutory period is the way one of them ends up retuned for\n\t// the other's reasons — the hazard pendingTTLBlocks' own note names below.\n\treSetWindowBlocks = int64(241_920) // ~14 days at 17_280 blocks/day\n\n\t// Moderation event names. Events carry act metadata and a category/act code\n\t// only — NEVER reason/annotation/title text (I6): events are unpurgeable\n\t// history, so leaking user text through one would defeat purge.\n\tmodActEvent    = \"ModAct\"    // a courtMod / meta bit change or annotation\n\tglobalActEvent = \"GlobalAct\" // a global-DAO bit / tier change\n\tpurgeEvent     = \"Purge\"     // a row/court text tombstone\n)\n\n// ---- the global moderator DAO (realm-wide) --------------------------------\n\n// daoState is the realm's global moderator set: the operator's legal backstop\n// (MODERATION.md §3.2). It generalizes directoryAdmin — the first court creator\n// bootstraps it as the sole member/admin. Powers: court tiers, an all-surface\n// text-redaction hide anywhere, clear-any-bit recovery, and purge (m-of-n).\ntype daoState struct {\n\tadmin   address\n\tmembers *bptree.BPTree // addr -\u003e true\n\tn       int\n\tpurgeM  int            // m-of-n for purge and windowed re-set\n\tpending *bptree.BPTree // actionKey -\u003e *approval\n}\n\nvar globalDAO *daoState\n\n// The seat count and threshold a bootstrapped DAO starts with. Named because a\n// PURE read has to report them without calling ensureGlobalDAO — that function\n// allocates, and check-read-purity refuses an exported read that allocates,\n// since the first caller would change what the second one sees. Two literals in\n// two files would be two things to keep in step; adminparams.gno reads these.\nconst (\n\tdaoBootstrapN = 1\n\tdaoBootstrapM = 1\n)\n\n// ensureGlobalDAO lazily migrates directoryAdmin into the DAO on first use.\nfunc ensureGlobalDAO() *daoState {\n\tif globalDAO != nil {\n\t\treturn globalDAO\n\t}\n\t// THIS PANIC IS IN NO MUTATION BATCH, AND THAT IS DELIBERATE. It fires only\n\t// when globalDAO is still nil AND directoryAdmin is still empty, and both are\n\t// package-globals that never reset: directoryAdmin is set by the first\n\t// StartCourt (court.gno) and globalDAO, once built, stays built. So a test\n\t// asserting this refusal would pass ALONE and fail TOGETHER with its package,\n\t// and check-isolation.py requires both — the guard is unpinnable by\n\t// construction. Note the near miss it protects against anyway: reading\n\t// DirectoryAdmin() before any court exists yields the ZERO address, which is\n\t// not a DAO member and not the admin, so calls made with it are silently\n\t// no-ops rather than refusals. A fixture that reads it must create its court\n\t// first.\n\tif directoryAdmin == \"\" {\n\t\tpanic(\"kourtv2: no court exists yet; the global DAO has no admin\")\n\t}\n\td := \u0026daoState{\n\t\tadmin:   directoryAdmin,\n\t\tmembers: bptree.NewBPTree32(),\n\t\tn:       daoBootstrapN,\n\t\tpurgeM:  daoBootstrapM,\n\t\tpending: bptree.NewBPTree32(),\n\t}\n\td.members.Set(directoryAdmin.String(), true)\n\tglobalDAO = d\n\treturn d\n}\n\nfunc isGlobalMod(who address) bool {\n\td := ensureGlobalDAO()\n\treturn d.members.Has(who.String())\n}\n\n// AddGlobalMod / RemoveGlobalMod / SetPurgeThreshold: admin-managed membership\n// (MODERATION.md §3.2; DAO-ification is V3, the shape matters more than the\n// governance for now).\nfunc AddGlobalMod(cur realm, who address) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\td := ensureGlobalDAO()\n\tif cur.Previous().Address() != d.admin {\n\t\tpanic(\"kourtv2: only the global DAO admin manages membership\")\n\t}\n\tseatGlobalMember(d, who)\n}\n\n// seatGlobalMember adds an address to the DAO's member set, or does nothing at\n// all if it is already in it.\n//\n// ITS TWO CALLERS HAD ALREADY DRIFTED ON THESE FOUR LINES, which is the reason\n// they are one block now. AddGlobalMod cleared the banked m-of-n approvals when\n// the member set changed and TransferGlobalAdmin did not, and that made the\n// transfer the STRICTLY BETTER route for an admin wanting to complete a stuck\n// purge: seat a fresh key without resetting the signatures banked against the\n// old set. Both clear now. Sharing the lines is what stops the next one added\n// here from reaching one caller and missing the other.\n//\n// ONLY THE ADD IS SHARED. Removal is not the mirror of it — RemoveGlobalMod\n// also has to pull purgeM down to the smaller n — and seating cannot put m\n// above n, so there is nothing to clamp on this side.\nfunc seatGlobalMember(d *daoState, who address) {\n\tif d.members.Has(who.String()) {\n\t\treturn\n\t}\n\td.members.Set(who.String(), true)\n\td.n++\n\tclearPendingOnMembershipChange(\u0026d.pending) // stale approvals must not carry over\n}\n\nfunc RemoveGlobalMod(cur realm, who address) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\td := ensureGlobalDAO()\n\tif cur.Previous().Address() != d.admin {\n\t\tpanic(\"kourtv2: only the global DAO admin manages membership\")\n\t}\n\tif who == d.admin {\n\t\tpanic(\"kourtv2: the admin cannot be removed; transfer admin first\")\n\t}\n\tif d.members.Has(who.String()) {\n\t\td.members.Remove(who.String())\n\t\td.n--\n\t\tif d.purgeM \u003e d.n {\n\t\t\td.purgeM = d.n\n\t\t}\n\t\tclearPendingOnMembershipChange(\u0026d.pending) // a removed member's approval must not still count\n\t}\n}\n\n// SetPurgeThreshold sets m for the m-of-n purge/re-set rule, 1..n.\nfunc SetPurgeThreshold(cur realm, m int) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\td := ensureGlobalDAO()\n\tif cur.Previous().Address() != d.admin {\n\t\tpanic(\"kourtv2: only the global DAO admin sets the purge threshold\")\n\t}\n\tif m \u003c 1 || m \u003e d.n {\n\t\tpanic(\"kourtv2: the purge threshold must be in 1..n\")\n\t}\n\tif m != d.purgeM {\n\t\t// Lowering the bar re-scores every banked signature against it: five\n\t\t// approvals gathered under 7-of-7 become executable the moment the\n\t\t// admin sets 5, at their sole discretion. The threshold is half of\n\t\t// \"who must agree\", so changing it invalidates consent just as\n\t\t// membership does.\n\t\tclearPendingOnMembershipChange(\u0026d.pending)\n\t}\n\td.purgeM = m\n}\n\n// TransferGlobalAdmin hands the admin seat to another member.\nfunc TransferGlobalAdmin(cur realm, to address) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\td := ensureGlobalDAO()\n\tif cur.Previous().Address() != d.admin {\n\t\tpanic(\"kourtv2: only the global DAO admin transfers the seat\")\n\t}\n\t// Seating an EXISTING member changes no set and clears nothing, here as in\n\t// AddGlobalMod: the banked approvals are consent to a membership, and this\n\t// path has not altered one.\n\tseatGlobalMember(d, to)\n\td.admin = to\n}\n\n// ---- per-court moderator set + per-claim hide state -----------------------\n\n// courtMod is one court's moderation state (MODERATION.md §3.1, §4). The set\n// decides by m-of-n (round-7 R3), so a single captured or decoy member can act\n// on nothing.\ntype courtMod struct {\n\tmembers *bptree.BPTree // addr -\u003e true\n\tn       int\n\tm       int // m-of-n decision threshold\n\n\tsuspended   bool\n\tsuspendedAt int64 // when the CURRENT suspension began (render)\n\t// suspendActByGlobal stamps the two GLOBAL DAO suspension verbs. The meta\n\t// staleness guard reads it so a verdict cannot silently overturn a global\n\t// decision taken after the appeal was filed — see meta.gno's guard 7.\n\tsuspendActByGlobal int64\n\tsuspendedSetID     string // WHICH membership was judged — a re-seat of the same one must not clear the flag\n\tsuspendedM         int    // ...and how tightly it had to agree; only TIGHTENING earns a lift\n\tsetActHeight       int64  // last membership change (for the meta staleness stamp)\n\tlastElectionAt     int64\n\t// installedByMeta: this set was seated by a meta verdict rather than by the\n\t// court's own creator or electorate. Such a set may not SEED claims — the\n\t// seeding waiver skips the CC deposit and fee while winner/answerer slices\n\t// still mint, so it is an economic privilege, and a captured meta must not\n\t// be able to hand an attacker-chosen principal a free claim faucet.\n\tinstalledByMeta bool\n\n\tcourtPurged    bool   // whole-court text tombstone (global DAO, m-of-n)\n\tcourtTombstone string // the category code\n\n\tclaims  *bptree.BPTree // claimID (be64) -\u003e *claimMod\n\tpending *bptree.BPTree // actionKey -\u003e *approval\n\n\t// boardSanctionEpoch is what makes every board freeze in the court clearable\n\t// in ONE write (boardmod.gno). A freeze records the epoch it was set under\n\t// and is live only while the two still match, so the recovery verb bumps this\n\t// instead of walking a tree of unknown size on the remedy path — which §7.5\n\t// of docs/CLAIM_BOARDS.md refuses everywhere else for the same reason.\n\tboardSanctionEpoch int64\n\n\t// The COURT-level act log. claimMod's log is keyed to a claim and a board\n\t// sanction is keyed to an ADDRESS, so slashes, freezes and their undos have\n\t// no claim to hang off. They land here, and renderModLog reads both.\n\tlog    []modAct\n\tlogSeq uint64\n\n\tfolders   *bptree.BPTree // folderID (be64) -\u003e *folder (folders.gno)\n\tfolderSeq uint64\n\n\t// Folders PROPOSED but not created: the payload of a `mod:newset` appeal.\n\t// Registered separately from the appeal for the reason RegisterModCandidate\n\t// exists — a name and a description do not fit in a claim title, and the\n\t// meta court's predate guard needs something with a height on it to compare.\n\t// A proposal creates nothing; only a carried verdict does.\n\tfolderProps   *bptree.BPTree // proposalID (be64) -\u003e *folderProposal\n\tfolderPropSeq uint64\n\n\t// The replacement election (modvote.gno) and the candidate registry it and\n\t// the meta court's `mod:setmods` peer path share.\n\tcandidates            *bptree.BPTree // candidateID (be64) -\u003e *modCandidate\n\tcandSeq               uint64\n\telection              *election\n\telectionSeq           uint64\n\telectionCooldownUntil     int64 // this court's OWN cooldown slot (not the per-claim flag field)\n\telectionCooldownUntilTime int64 // its wall-clock twin, written at the same moment\n\tcreatorUnseated       bool  // an election has installed a set; the creator no longer appoints\n\n\t// election / strip-index state land in modvote.gno / the strip module; they\n\t// hang off this struct so a court's whole moderation footprint is one\n\t// lazily-created object.\n}\n\n// claimMod is the per-claim hide word + log. Created only for a claim that is\n// actually moderated (so an unmoderated claim costs nothing). Bits are\n// independent (union): an item lists only if none is set.\ntype claimMod struct {\n\tcourt  bool // courtMod hide bit\n\tmeta   bool // meta-court hide bit (set/cleared by ExecuteMetaVerdict)\n\tglobal bool // global DAO all-surface text-redaction hide\n\n\tpurged    bool   // claim title tombstoned (global purge)\n\ttombstone string // the category code shown in place of text\n\n\t// One claim's board, closed by a moderator act and auto-expiring\n\t// (boardmod.gno). The CLAIM stays fully live — answers, stakes, votes and the\n\t// verdict are untouched; only comments stop. Expiry is lazy, and the epoch\n\t// stamp is what lets the global recovery verb clear it in one write.\n\tboardFrozenUntil     int64\n\tboardFrozenUntilTime int64 // unix seconds beside it; 0 on freezes predating the stamps\n\tboardFrozenEpoch     int64\n\t// The re-freeze gap, for the same reason an address freeze has one: without\n\t// it freezeMaxBlocks bounds nothing, because the set re-freezes in the next\n\t// block. This verb shipped with the ceiling and without the gap — so the one\n\t// freeze that could be held at a 100% duty cycle was the one aimed at a whole\n\t// claim's board rather than at one person. Neither the undo nor the sanction\n\t// epoch clears it; see standing.gno's frozenGapUntil.\n\tboardFrozenGapUntil     int64\n\tboardFrozenGapUntilTime int64 // its wall-clock twin, written at the same moment\n\n\t// boardPurgeAll: the global DAO has decided this claim's whole board goes.\n\t// The DECISION is voted once; the mechanical completion is not re-voted,\n\t// because a purge that needed m-of-n per 64-row batch would make a 500-row\n\t// board eight coordinated votes under a 72-hour clock. Any global DAO member\n\t// may advance the cursor once the flag is set, and the flag also makes the\n\t// order self-evident at render before the last batch has run.\n\tboardPurgeAll bool\n\n\t// There were once three more stamps here — courtActByOthers, metaActByOthers\n\t// and globalActHeight — carrying a per-claim \"a third party already acted\"\n\t// term for the meta staleness guard. That refusal was deliberately REMOVED\n\t// (see verbUnhide in meta.gno: refusing on a same-direction third-party\n\t// clear let a moderator re-hide for free), and nothing ever read them again.\n\t// They are gone rather than left looking load-bearing; the mod log carries\n\t// the same who/when for render. A stripEnteredAt field went the same way —\n\t// the strip key already encodes the deadline it would have duplicated.\n\tglobalClearedAt     int64 // for the windowed-m-of-n re-set rule\n\tglobalClearedAtTime int64 // unix seconds beside it; 0 on clears predating the stamps\n\texecutedAt      int64 // meta-verdict cooldown anchor (module 7)\n\texecutedAtTime  int64 // unix seconds beside it; 0 on verdicts predating the stamps\n\n\tlogSeq uint64\n\tlog    []modAct\n}\n\n// modAct is one append-only moderation-log row. rowID lets purge address a\n// single row.\n//\n// code is an act tag, and it is NOT AN ENUM. This said \"a category/act code (an\n// enum), never user text\", and that was wrong in the way that matters: eight of\n// the eleven acts that write a row do pass a fixed string (\"hide\", \"unhide\",\n// \"meta-clear\" and the like), but three CONCATENATE a caller-supplied statutory\n// code into it — \"global-hide:\"+categoryCode, \"purge:\"+code and\n// \"purgerow:\"+code — and mustCategoryCode validates LENGTH ONLY, with no alphabet\n// and no membership test. So those three carry free text with every punctuation\n// mark available.\n//\n// renderModLog therefore sanitises code exactly as it sanitises reason, and that\n// is not belt-and-braces. A \"never user text\" claim on a field that IS user text\n// is precisely what licenses a later reader to render it raw, which is how the\n// same mistake reached ccwrap's page. Pinned by\n// TestAPurgeCodeCannotWriteMarkupIntoTheModLog.\n//\n// reason is the stored text, sanitized at render, and purgeable.\ntype modAct struct {\n\trowID  uint64\n\tactor  address\n\tcode   string\n\treason string\n\theight int64\n\tpurged bool\n}\n\n// reSetWindowOpen reports whether a global clear is still inside the §512(g)\n// counter-notice window, so a re-set needs m-of-n rather than one key.\n//\n// TWO GATES ASK THIS, on two different records: a claim's redaction here and a\n// board row's in boardlegal.gno. They are one statutory period, so they get one\n// predicate rather than two copies that could be converted separately — the\n// lesson approvalStale records one screen up.\nfunc reSetWindowOpen(clearedAtTime, clearedAt, now int64) bool {\n\tpassed, known := pastDeadline(clearedAtTime, blocksToSecs(reSetWindowBlocks))\n\treturn (known \u0026\u0026 !passed) || (!known \u0026\u0026 now \u003c clearedAt+reSetWindowBlocks)\n}\n\n// pendingTTLBlocks is how long an unfired m-of-n approval stays valid: 7 days,\n// the same duration as the default votingBlocks. This is decideWindowBlocks,\n// the realm's unit for \"a body gets this long to decide\" — and a set of at most 32\n// self-selected moderators should not get longer than a whole court electorate\n// gets for a dispute. §3.2's own runbook targets an m-of-n purge \"within 72h\",\n// so a week is already 2.3× the operating target.\n//\n// Deliberately NOT reSetWindowBlocks (14d): that measures the DMCA §512(g)\n// counter-notice period, an external statutory clock that counsel must be able\n// to retune alone. Coupling an internal quorum rule to it means one of the two\n// eventually gets changed for the other's reasons.\nconst pendingTTLBlocks = decideWindowBlocks\n\n// approval accumulates distinct-member approvals for an m-of-n action.\n//\n// openedAt is read (unlike the actor0 that was deleted alongside it in v0.16\n// and stays deleted, still having no reader). It exists because THE REAL DEFECT\n// here was never storage — approvals are self-paid via the storage deposit,\n// bounded by a derived key space, and walked by nothing. It is that an approval\n// could never go stale: m-of-n meant \"m members agreed at some point in\n// history\", not \"m members agree now\". A signature given in January sat at m-1\n// and could be completed in November by one careless colleague, on an\n// irreversible purge its author would have withdrawn if withdrawing existed.\ntype approval struct {\n\tapprovers    *bptree.BPTree // addr -\u003e true\n\treason       string\n\topenedAt     int64\n\topenedAtTime int64 // unix seconds; 0 on entries opened before the stamps\n}\n\n// approvalStale reports whether an unfired entry has outlived its TTL.\n//\n// ONE PREDICATE FOR THE THREE PLACES THAT ASK — approveAction, pendingOpenedAt\n// and PendingApproval. It was written out at each of them, kept honest by\n// adjacency (see pendingOpenedAt's note), which held while the test was one line\n// and stopped holding the moment it became a two-clock test: three copies of a\n// stamp-then-height fallback are three chances to convert two of them.\n//\n// The TTL converts at the CALL SITE rather than through a seconds constant of\n// its own, because pendingTTLBlocks is not a number here — it is\n// decideWindowBlocks, \"the realm's unit for a body gets this long to decide\". A\n// parallel constant would keep its own copy of that duration and stop tracking a\n// tune of the original, which is the coupling the const's own comment refuses\n// elsewhere (\"one of the two eventually gets changed for the other's reasons\").\nfunc approvalStale(a *approval) bool {\n\tpassed, known := pastDeadline(a.openedAtTime, blocksToSecs(pendingTTLBlocks))\n\treturn (known \u0026\u0026 passed) || (!known \u0026\u0026 heightNow() \u003e= a.openedAt+pendingTTLBlocks)\n}\n\n// ensureMod lazily creates a court's moderation object with a 1-of-1 creator\n// set (the creator bootstraps; an election or AppointMods reshapes it).\nfunc ensureMod(c *Court) *courtMod {\n\tif c.mod != nil {\n\t\treturn c.mod\n\t}\n\tcm := \u0026courtMod{\n\t\tmembers: bptree.NewBPTree32(),\n\t\tn:       1,\n\t\tm:       1,\n\t\tclaims:  bptree.NewBPTree32(),\n\t\tpending: bptree.NewBPTree32(),\n\t\tfolders: bptree.NewBPTree32(),\n\t}\n\tcm.members.Set(c.admin.String(), true)\n\tc.mod = cm\n\treturn cm\n}\n\n// AppointMods lets the court creator set the moderator addresses and the m-of-n\n// threshold, until a replacement election unseats the appointment power\n// (modvote.gno). Addresses-only; no free text.\nfunc AppointMods(cur realm, courtSlug string, m int, mods ...address) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tc := mustCourt(courtSlug)\n\tif cur.Previous().Address() != c.admin {\n\t\tpanic(\"kourtv2: only the court creator appoints moderators (until an election)\")\n\t}\n\tif c.mod != nil \u0026\u0026 c.mod.creatorUnseated {\n\t\tpanic(\"kourtv2: an election has installed a set; the creator's appointment power is spent\")\n\t}\n\t// A suspended set may not be re-appointed around. AppointMods is\n\t// creator-only and FREE, so if it cleared (or ignored) the suspension a\n\t// suspended creator would self-unsuspend for zero cost — turning meta's\n\t// bonded, voted disarm into a no-op. Only the bonded/voted peers (an\n\t// election install or a meta verdict) clear a suspension.\n\tif c.mod != nil \u0026\u0026 c.mod.suspended {\n\t\tpanic(\"kourtv2: this moderator set is suspended; only an election or a meta verdict may reinstate it\")\n\t}\n\t// Canonicalize FIRST, then validate: checking m against the raw list let\n\t// {A,A,B} with m=3 install n=2/m=3 — a set in which no action can ever reach\n\t// its own threshold, including UnhideItem, so any standing hide became\n\t// locally permanent.\n\tcanon := canonicalMembers(mods)\n\tif len(canon) == 0 {\n\t\tpanic(\"kourtv2: a moderator set may not be empty\")\n\t}\n\tif m \u003c 1 || m \u003e len(canon) {\n\t\tpanic(\"kourtv2: the m-of-n threshold must be in 1..n\")\n\t}\n\tcm := ensureMod(c)\n\tfresh := bptree.NewBPTree32()\n\tn := 0\n\tfor _, a := range canon {\n\t\tfresh.Set(a.String(), true)\n\t\tn++\n\t}\n\tcm.members = fresh\n\tcm.n = n\n\tcm.m = m\n\tcm.setActHeight = heightNow()\n\tcm.installedByMeta = false // a creator appointment is a local act\n\tclearPendingOnMembershipChange(\u0026cm.pending)\n}\n\nfunc requireMod(cm *courtMod, who address) {\n\tif !cm.members.Has(who.String()) {\n\t\tpanic(\"kourtv2: not a moderator of this court\")\n\t}\n}\n\n// clearPendingOnMembershipChange discards a body's pending m-of-n approvals.\n//\n// Approvals record approver ADDRESSES and are never re-validated at fire time,\n// so a surviving entry lets a deposed moderator's signature still count toward\n// the threshold — including toward an irreversible purge. Any change to WHO is\n// in a set, or to HOW MANY of them must agree, therefore invalidates every\n// approval collected under the old rule.\n//\n// This existed only as a comment and four hand-written copies of one line, and\n// the three membership paths added later each forgot it (three identical\n// adversarial reviewers found the same hole independently). Naming an invariant\n// is not enforcing it; a helper every mutating path calls is.\nfunc clearPendingOnMembershipChange(pending **bptree.BPTree) {\n\t*pending = bptree.NewBPTree32()\n}\n\n// Pending m-of-n approvals are DISCARDED whenever the set changes. They record\n// approver addresses and are never re-validated at fire time, so a surviving\n// entry lets a deposed moderator's signature still count toward the threshold —\n// including toward an irreversible purge. Same reasoning for the global DAO.\n//\n// requireActiveMod additionally forbids the act while the set is suspended —\n// a suspended set may only CLEAR its own bits (de-escalatory), never hide,\n// annotate, seed, or fold (MODERATION.md §3.1).\nfunc requireActiveMod(cm *courtMod, who address) {\n\trequireMod(cm, who)\n\tif cm.suspended {\n\t\tpanic(\"kourtv2: this moderator set is suspended; only clearing its own bits is allowed\")\n\t}\n}\n\n// isActiveMod answers what requireActiveMod refuses, without the panic: it is\n// for the paths that PRICE a caller rather than stop them. A suspended set's\n// member is deliberately not active here — a suspension leaves a set able only\n// to clear its own bits, so while it holds, its members are strangers to\n// everything else and pay a stranger's bond.\nfunc isActiveMod(cm *courtMod, who address) bool {\n\treturn !cm.suspended \u0026\u0026 cm.members.Has(who.String())\n}\n\n// requireEdgeRemover gates removal of a claim-to-claim edge: its author may\n// always take it back, and so may an active moderator of the court.\n//\n// ONE COPY BECAUSE THIS BLOCK ALREADY DRIFTED. RemoveAssociation tested\n// `c.mod == nil` where RemoveSupersede called ensureMod, and those are not the\n// same question: a nil c.mod means nobody has moderated this court YET, not that\n// it has no moderators — ensureMod seeds the creator. The old form refused a\n// genuine moderator on every court where no folder had ever been filed, which is\n// most of them, and contradicted RemoveAssociation's own doc comment. Fixing it\n// meant copying this shape across, after which association.gno's note read \"see\n// supersede.gno for the full note\" — a site that can only explain itself by\n// pointing at another site.\n//\n// The membership test is spelled out rather than left to requireMod so the\n// refusal keeps naming BOTH parties who may act. \"not a moderator of this court\"\n// is true and unhelpful to someone who is simply not the author.\nfunc requireEdgeRemover(c *Court, author, who address) {\n\tif author == who {\n\t\treturn\n\t}\n\tcm := ensureMod(c)\n\tif !cm.members.Has(who.String()) {\n\t\tpanic(\"kourtv2: only the edge's author or a court moderator may remove it\")\n\t}\n\trequireActiveMod(cm, who)\n}\n\n// mustCategoryCode gates the statutory code every PURGE carries. Purge is the\n// LEGAL removal — it erases text behind a code and takes a global-DAO threshold to\n// fire — and the code is the record of WHY, so an empty one makes the act\n// unauditable and an unbounded one makes the record itself the abuse.\n//\n// ONE SITE FOR FOUR VERBS. This block was written out inline in PurgeClaim,\n// PurgeCourt, PurgeModLogRow and PurgeFolder, four byte-identical copies of the\n// same three lines. None had drifted yet, which is the only reason collapsing them\n// is a refactor and not a bug fix: two copies of one check is how the second comes\n// to disagree with the first, and four copies on the realm's most\n// authority-sensitive path is four chances. It also made the boundary unpinnable —\n// a corpus row needs an anchor that matches once, and three identical texts in one\n// file have none.\n// mustCategoryCode bounds the code by LENGTH ONLY, deliberately, and the reason\n// is worth stating because an alphabet here looks like an easy win and is not.\n//\n// Where this value ends up: emitPurge writes it into an event, and an event is\n// permanent — so this is the one field on the purge path where a bad value cannot\n// be undone afterwards. That argues for an alphabet at the door. Against it: the\n// escaping at render is what actually defends the pages, it is TESTED on four\n// surfaces with a code that forges structure, and `reason` sits beside this field\n// carrying prose that can never be constrained at all. Narrowing the code would\n// retire a live, exercised defence for one field while its free-text sibling\n// keeps needing it — strengthening the lock on one of two doors.\n//\n// So the code stays free text and every surface escapes it. What callers get\n// instead is this note: purge is m-of-n precisely because it is irreversible, and\n// the code is the part of it that outlives the claim.\n// CHARACTERS, like every other cap in this realm, and like checkReason ten lines\n// below sharing this very constant. This counted BYTES until it was measured —\n// the one cap runeLen's sweep did not convert, which left the two halves of\n// maxReasonLen disagreeing about what maxReasonLen means.\n//\n// The comment above says the code stays FREE TEXT, and that is exactly what made\n// the byte count wrong: a statutory category written in Cyrillic ran out at about\n// 100 characters and one in Japanese at about 66, against a stated 200. The\n// refusal does not mention length either, so a moderator over the byte bound was\n// told \"purge needs a statutory category code\" about a code they had just\n// written — the message reads as MISSING, not TOO LONG.\n//\n// Strictly more permissive, so nothing that was accepted becomes refused; the\n// empty case is unaffected, since a string is zero bytes exactly when it is zero\n// runes. The extra storage is paid for by the caller's deposit, which is the same\n// trade runeLen's own note records.\nfunc mustCategoryCode(code string) {\n\tif len(code) == 0 || runeLen(code) \u003e maxReasonLen {\n\t\tpanic(\"kourtv2: purge needs a statutory category code\")\n\t}\n}\n\nfunc checkReason(reason string) {\n\tif runeLen(reason) \u003e maxReasonLen {\n\t\tpanic(\"kourtv2: a moderation reason is at most 200 characters\")\n\t}\n}\n\nfunc ensureClaimMod(c *Court, cm *courtMod, claimID uint64) *claimMod {\n\t_ = mustClaim(c, claimID) // the claim must exist\n\tk := beClaimKey(claimID)\n\tif v := cm.claims.Get(k); v != nil {\n\t\treturn v.(*claimMod)\n\t}\n\tclm := \u0026claimMod{}\n\tcm.claims.Set(k, clm)\n\treturn clm\n}\n\nfunc lookupClaimMod(cm *courtMod, claimID uint64) *claimMod {\n\tif v := cm.claims.Get(beClaimKey(claimID)); v != nil {\n\t\treturn v.(*claimMod)\n\t}\n\treturn nil\n}\n\n// appendLog records an act and returns its rowID.\nfunc (clm *claimMod) appendLog(actor address, code, reason string) uint64 {\n\tclm.logSeq++\n\tclm.log = append(clm.log, modAct{\n\t\trowID:  clm.logSeq,\n\t\tactor:  actor,\n\t\tcode:   code,\n\t\treason: reason,\n\t\theight: heightNow(),\n\t})\n\treturn clm.logSeq\n}\n\n// appendCourtLog records an act that belongs to the COURT rather than to a\n// claim, and returns its rowID. Row ids are drawn from a separate sequence, so a\n// court row and a claim row may share a number — PurgeCourtLogRow and\n// PurgeModLogRow are different verbs and each addresses only its own log.\nfunc (cm *courtMod) appendCourtLog(actor address, code, reason string) uint64 {\n\tcm.logSeq++\n\tcm.log = append(cm.log, modAct{\n\t\trowID:  cm.logSeq,\n\t\tactor:  actor,\n\t\tcode:   code,\n\t\treason: reason,\n\t\theight: heightNow(),\n\t})\n\treturn cm.logSeq\n}\n\n// approveAction records who's approval for key and returns (fire, reason). When\n// the m-th distinct member approves, the pending entry is cleared and fire=true;\n// with m==1 the first call fires. Re-approving is a no-op (returns false).\n//\n// EXPIRY IS LAZY, AND ONLY LAZY — there is no sweep entrypoint, by design.\n//\n// The deadline is FIXED at the first approval, not slid forward by each new\n// signer. Sliding would give an entry up to n×TTL of life (32 × 7 days ≈ 224\n// days), reintroducing the problem; and a fixed death height is a public,\n// deterministic function of chain state, identical for attacker and defender,\n// which is what makes this unweaponisable. A permissionless sweep with a hard\n// cutoff WOULD be a veto: watch for the m-th signature, front-run it, and the\n// entry vanishes — profitably, since the storage refund pays the caller. Lazy\n// eviction has no such surface. The predicate is only ever evaluated by a\n// member already authorised for that exact key, and eviction is a REPLACEMENT,\n// not a deletion: the evictor's own signature seeds the fresh entry, so they\n// cannot leave the key empty. Racing to touch an already-expired key is not a\n// veto, it is authorship of a new proposal, and the others can re-sign and fire\n// in the same block.\n//\n// It is also the reclaim. The overwriting Set drops the old approval and its\n// approvers tree in the same transaction, and gno refunds the released bytes to\n// that caller — so every entry anyone revisits is collected automatically, and\n// the entries nobody revisits are exactly the ones whose existence costs\n// nothing. Expiry is silent: no cooldown, no cap, no event. Punishing a lapse\n// would tax the honest first mover, and an m-of-n set whose members are afraid\n// to sign first never acts at all.\nfunc approveAction(pending *bptree.BPTree, key string, who address, reason string, m int) (bool, string) {\n\tnow := heightNow()\n\tvar a *approval\n\tif v := pending.Get(key); v != nil {\n\t\ta = v.(*approval)\n\t\tif approvalStale(a) {\n\t\t\ta = nil // stale by rule; this caller opens a fresh proposal\n\t\t}\n\t}\n\tif a == nil {\n\t\ta = \u0026approval{approvers: bptree.NewBPTree32(), reason: reason,\n\t\t\topenedAt: now, openedAtTime: nowTime()}\n\t}\n\tif a.approvers.Has(who.String()) {\n\t\treturn false, \"\"\n\t}\n\ta.approvers.Set(who.String(), true)\n\tif a.approvers.Size() \u003e= m {\n\t\tpending.Remove(key)\n\t\treturn true, a.reason\n\t}\n\tpending.Set(key, a)\n\treturn false, \"\"\n}\n\n// pendingOpenedAt is the height an OPEN m-of-n proposal started at, or 0 when\n// there is none — either because nobody has signed yet or because the one on\n// file has gone stale and approveAction is about to replace it.\n//\n// IT SITS HERE, TOUCHING approveAction, BECAUSE IT ASKS THAT FUNCTION'S\n// STALENESS QUESTION. It used to REPEAT it, guarded only by adjacency — the two\n// `openedAt+pendingTTLBlocks` tests were eight lines apart so a reader changing\n// either saw the other. That guard was sized for a one-line test. Converting the\n// TTL to the wall clock made it a stamp-then-height fallback, at which point\n// three copies were three chances to convert two, so both call sites and the\n// reader now share approvalStale. Adjacency is kept anyway; it costs nothing.\n//\n// WHAT NEEDS IT: a bulk act must consent to a FIXED set. DisapproveAllAssociations\n// burns money, and between the first signature and the m-th a stranger may post\n// an honest bond that no moderator has ever looked at. Bounding the burn to\n// edges that predate the proposal is what keeps \"unjudged means approved\" true\n// for them, and it is also what stops the mirror attack — a griefer who could\n// invalidate the proposal by adding one edge a day would stall the remedy\n// forever.\nfunc pendingOpenedAt(pending *bptree.BPTree, key string) int64 {\n\tv := pending.Get(key)\n\tif v == nil {\n\t\treturn 0\n\t}\n\ta := v.(*approval)\n\tif approvalStale(a) {\n\t\treturn 0\n\t}\n\treturn a.openedAt\n}\n\n// ---- court-moderator hide / unhide (m-of-n) -------------------------------\n\n// HideItem removes a claim from this court's discovery surfaces. Render-layer:\n// the claim's lifecycle is untouched, and it stays reachable by direct link\n// (with a banner). m-of-n.\nfunc HideItem(cur realm, courtSlug string, claimID uint64, reason string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\trequireActiveMod(cm, who)\n\tcheckReason(reason)\n\t_ = mustClaim(c, claimID) // the claim must exist, whether or not this fires\n\t// The re-hide cooldown (I8). Without reading executedAt, a moderator who\n\t// LOST an unhide appeal — after the appellant spent weeks and real bonds —\n\t// re-hid in the very next block for free, nullifying the restorative half of\n\t// the appeals layer. Global-DAO verbs and meta executions do not route here.\n\t// Read through lookupClaimMod: an absent row means no verdict ever landed.\n\t//\n\t// THE COOLDOWN IS THE VOTE'S OWN LENGTH, so it has to be measured in the same\n\t// unit the appellant experienced. Counted in blocks it was one vote long only\n\t// at 5s a block: faster, and the loser of an appeal waits less than the appeal\n\t// itself took. votingBlocks is a court parameter denominated in blocks, so it\n\t// converts at the call site rather than through a constant of its own.\n\tif clm := lookupClaimMod(cm, claimID); clm != nil \u0026\u0026 clm.executedAt != 0 {\n\t\tif passed, known := pastDeadline(clm.executedAtTime, blocksToSecs(c.params.votingBlocks)); (known \u0026\u0026 !passed) ||\n\t\t\t(!known \u0026\u0026 heightNow() \u003c clm.executedAt+c.params.votingBlocks) {\n\t\t\tpanic(\"kourtv2: a review verdict just cleared this; the set may not re-hide yet\")\n\t\t}\n\t}\n\tfire, r := approveAction(cm.pending, \"hide:\"+strconv.FormatUint(claimID, 10), who, reason, cm.m)\n\tif !fire {\n\t\treturn\n\t}\n\t// Allocate only now that the act is FIRING. Allocating before the m-of-n\n\t// resolves let a single member of an m\u003e1 set mint an empty claimMod per\n\t// claim — rows that carry no act, cost storage, and that renderModLog and\n\t// every claims-tree walk must then step over.\n\tclm := ensureClaimMod(c, cm, claimID)\n\tclm.court = true\n\tclm.appendLog(who, \"hide\", r)\n\temitModAct(c.id, claimID, \"hide\", who)\n}\n\n// UnhideItem clears this court's own hide bit. Allowed even while suspended (it\n// is strictly de-escalatory). m-of-n.\nfunc UnhideItem(cur realm, courtSlug string, claimID uint64, reason string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\trequireMod(cm, who) // NOT requireActiveMod — clearing own bits survives suspension\n\tcheckReason(reason)\n\tclm := lookupClaimMod(cm, claimID)\n\tif clm == nil || !clm.court {\n\t\tpanic(\"kourtv2: this court is not hiding that claim\")\n\t}\n\tfire, r := approveAction(cm.pending, \"unhide:\"+strconv.FormatUint(claimID, 10), who, reason, cm.m)\n\tif !fire {\n\t\treturn\n\t}\n\tclm.court = false\n\tclm.appendLog(who, \"unhide\", r)\n\temitModAct(c.id, claimID, \"unhide\", who)\n}\n\n// ---- global DAO: redaction, clear-any recovery, purge ---------------------\n\n// GlobalHide sets the all-surface text-redaction bit: on every surface the row\n// renders banner + IDs + state + deadlines + money, no user text (MODERATION.md\n// §3.2). Single-key to SET (legal speed); reason is a category code only. The\n// DMCA disable-access verb.\nfunc GlobalHide(cur realm, courtSlug string, claimID uint64, categoryCode string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tif !isGlobalMod(who) {\n\t\tpanic(\"kourtv2: only a global DAO member may global-hide\")\n\t}\n\tcheckReason(categoryCode)\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\t_ = mustClaim(c, claimID) // the claim must exist, whether or not this fires\n\tnow := heightNow()\n\t// Windowed m-of-n re-set (round-4 F5): re-setting a redaction a global act\n\t// cleared within the counter-notice window needs m-of-n, so a lone rogue\n\t// key can't win the reversal race and camp a competitor redacted.\n\tif prev := lookupClaimMod(cm, claimID); prev != nil \u0026\u0026 !prev.global \u0026\u0026\n\t\tprev.globalClearedAt != 0 \u0026\u0026 reSetWindowOpen(prev.globalClearedAtTime, prev.globalClearedAt, now) {\n\t\td := ensureGlobalDAO()\n\t\tfire, _ := approveAction(d.pending, \"reset:\"+c.id+\":\"+strconv.FormatUint(claimID, 10), who, categoryCode, d.purgeM)\n\t\tif !fire {\n\t\t\treturn\n\t\t}\n\t}\n\tclm := ensureClaimMod(c, cm, claimID) // only once the hide is certain\n\tclm.global = true\n\tclm.appendLog(who, \"global-hide:\"+categoryCode, \"\")\n\temitGlobalAct(c.id, claimID, \"global-hide\", who)\n}\n\n// GlobalClear clears the global bit — the ONLY way it is reversed (never by\n// silence/timer/meta). Explicit, logged. This is the DMCA put-back.\nfunc GlobalClear(cur realm, courtSlug string, claimID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tif !isGlobalMod(who) {\n\t\tpanic(\"kourtv2: only a global DAO member may clear a global hide\")\n\t}\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\tclm := lookupClaimMod(cm, claimID)\n\tif clm == nil || !clm.global {\n\t\tpanic(\"kourtv2: no global hide on that claim\")\n\t}\n\tclm.global = false\n\tclm.globalClearedAt = heightNow()\n\tclm.globalClearedAtTime = nowTime()\n\tclm.appendLog(who, \"global-clear\", \"\")\n\temitGlobalAct(c.id, claimID, \"global-clear\", who)\n}\n\n// ClearAnyBit is the recovery power: a global DAO member may clear ANY hide bit\n// (courtMod or meta) and lift a court's suspension flag, so a captured meta\n// court's bits/suspensions are always reversible (MODERATION.md §3.2, round-2).\nfunc ClearAnyBit(cur realm, courtSlug string, claimID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tif !isGlobalMod(who) {\n\t\tpanic(\"kourtv2: only a global DAO member may clear bits (recovery)\")\n\t}\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\tclm := lookupClaimMod(cm, claimID)\n\tif clm == nil {\n\t\tpanic(\"kourtv2: nothing to clear on that claim\")\n\t}\n\tclm.court = false\n\tclm.meta = false\n\tclm.appendLog(who, \"clear-any\", \"\")\n\temitGlobalAct(c.id, claimID, \"clear-any\", who)\n}\n\n// ClearCourtSuspension lifts a court's suspension flag (the recovery power\n// reaching the flag, not just bits — round-2 M-A22).\nfunc ClearCourtSuspension(cur realm, courtSlug string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tif !isGlobalMod(who) {\n\t\tpanic(\"kourtv2: only a global DAO member may clear a suspension (recovery)\")\n\t}\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\tif !cm.suspended {\n\t\tpanic(\"kourtv2: that court's set is not suspended\")\n\t}\n\tcm.suspended = false\n\tcm.suspendActByGlobal = heightNow()\n\temitGlobalAct(c.id, 0, \"clear-suspension\", who)\n}\n\n// PurgeClaim tombstones a claim's title behind a category code on EVERY surface\n// (the one power that removes text). m-of-n. Irreversible, unappealable. Never\n// touches a vote or lifecycle (I1/I2): the deposit still refunds, the verdict\n// still settles — only the text is gone.\nfunc PurgeClaim(cur realm, courtSlug string, claimID uint64, categoryCode string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\td := ensureGlobalDAO()\n\tif !d.members.Has(who.String()) {\n\t\tpanic(\"kourtv2: only a global DAO member may purge\")\n\t}\n\tmustCategoryCode(categoryCode)\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\t_ = mustClaim(c, claimID) // the claim must exist, whether or not this fires\n\tfire, code := approveAction(d.pending, \"purge:\"+c.id+\":\"+strconv.FormatUint(claimID, 10), who, categoryCode, d.purgeM)\n\tif !fire {\n\t\treturn\n\t}\n\tclm := ensureClaimMod(c, cm, claimID) // only once the purge is certain\n\tclm.purged = true\n\tclm.tombstone = code\n\tif c.id == metaSlug {\n\t\t// A purge BEFORE the binding persists makes the appeal render-only; a\n\t\t// purge after it never blocks execution (meta.gno).\n\t\tpoisonMetaParse(claimID)\n\t}\n\tclm.appendLog(who, \"purge:\"+code, \"\")\n\temitPurge(c.id, claimID, code, who)\n}\n\n// GlobalSuspendSet lets the global DAO SET a court's suspension, not only clear\n// it (setmods vet): the global verb set could previously only lower that flag,\n// so a hostile-but-not-yet-replaceable set had no fast global disarm. m-of-n —\n// disarming a court's moderation is consequential and is never time-critical\n// the way a legal-hold hide is.\nfunc GlobalSuspendSet(cur realm, courtSlug string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\td := ensureGlobalDAO()\n\tif !d.members.Has(who.String()) {\n\t\tpanic(\"kourtv2: only a global DAO member may suspend a moderator set\")\n\t}\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\tif cm.suspended {\n\t\tpanic(\"kourtv2: that set is already suspended\")\n\t}\n\tfire, _ := approveAction(d.pending, \"gsuspend:\"+c.id, who, \"\", d.purgeM)\n\tif !fire {\n\t\treturn\n\t}\n\tsuspendSet(c, who)\n\tcm.suspendActByGlobal = heightNow()\n\temitGlobalAct(c.id, 0, \"global-suspend-set\", who)\n}\n\n// ResetModSet is the SET-LEVEL global recovery verb (setmods vet, unanimous):\n// `ClearAnyBit` is per-claim, so before this existed a hostile moderator set —\n// e.g. puppets seated by a captured meta court — was globally irreversible, and\n// the constitution's \"backstopped by the global DAO\" claim was false for\n// installs. Whack-a-mole on individual bits does not work against a set that\n// keeps setting new ones, and the only other set-level hammer was the nuclear,\n// permanent PurgeCourt.\n//\n// It DISARMS rather than appoints: the set is emptied, so nobody can act until\n// the court's own electorate installs one (or, if no election has yet unseated\n// them, the creator re-appoints). That preserves subsidiarity — the global DAO\n// gets a cure, never the power to choose a court's moderators. m-of-n, because\n// wiping a set is consequential.\nfunc ResetModSet(cur realm, courtSlug string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\td := ensureGlobalDAO()\n\tif !d.members.Has(who.String()) {\n\t\tpanic(\"kourtv2: only a global DAO member may reset a moderator set\")\n\t}\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\tfire, _ := approveAction(d.pending, \"resetmods:\"+c.id, who, \"\", d.purgeM)\n\tif !fire {\n\t\treturn\n\t}\n\tcm.members = bptree.NewBPTree32()\n\tcm.n = 0\n\tcm.m = 1                                    // n==0 means nobody is a member, so nothing can act\n\tclearPendingOnMembershipChange(\u0026cm.pending) // stale approvals die with the set\n\tcm.suspended = false\n\t// AND EVERY LIVE BOARD FREEZE DIES WITH THE SET. A reset takes n to 0, so\n\t// nobody is left who could unfreeze — and a freeze that outlives the set that\n\t// imposed it is a sanction with no author, unliftable until it expires. One\n\t// integer clears them all (boardmod.gno's sanction epoch); the alternative is\n\t// an unbounded walk on the remedy path.\n\tcm.boardSanctionEpoch++\n\tcm.setActHeight = heightNow()\n\temitGlobalAct(c.id, 0, \"reset-mod-set\", who)\n}\n\n// PurgeCourt tombstones a whole court's text and gates new claims: on a purged\n// court `OpenClaim`/`OpenClaimSeeded` refuse (constitution carve-out #2), so an\n// illegal-theme court accretes no new content. Existing lifecycle still runs to\n// completion (I1) — stakers unwind, verdicts settle, deposits refund; only new\n// content and text are stopped. m-of-n.\nfunc PurgeCourt(cur realm, courtSlug string, categoryCode string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\td := ensureGlobalDAO()\n\tif !d.members.Has(who.String()) {\n\t\tpanic(\"kourtv2: only a global DAO member may purge\")\n\t}\n\tmustCategoryCode(categoryCode)\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\tfire, code := approveAction(d.pending, \"purgecourt:\"+c.id, who, categoryCode, d.purgeM)\n\tif !fire {\n\t\treturn\n\t}\n\tcm.courtPurged = true\n\tcm.courtTombstone = code\n\temitPurge(c.id, 0, code, who)\n}\n\n// courtIsPurged is the entry gate (MODERATION.md §2 build guardrail).\n//\n// A PREDICATE, NOT A LIST. This comment used to enumerate its two readers, and\n// the list had already drifted to five before anyone noticed — which is how an\n// enumeration guards nothing. The rule it was standing in for:\n//\n//\tread it on a path that BEGINS a commitment; never on one that settles or\n//\treleases a commitment already made.\n//\n// So OpenClaim, OpenClaimP, OpenClaimSeeded, PostComment and UpvoteComment read\n// it, and PostAnswer, Stake, every withdraw and every settlement path do not. A\n// purged court must stop accepting new business without stranding money already\n// in it, and a gate on a release path is exactly how the second half breaks.\n// check-epoch-coherence.py arm 16 holds the census — the count AND the\n// enclosing-function allowlist — so the next reader added has to say which side\n// of the line it is on.\nfunc courtIsPurged(c *Court) bool {\n\treturn c.mod != nil \u0026\u0026 c.mod.courtPurged\n}\n\n// CourtPurged is the render/test read.\nfunc CourtPurged(courtSlug string) bool {\n\treturn courtIsPurged(mustCourt(courtSlug))\n}\n\n// ---- meta / suspension internal hooks (called by ExecuteMetaVerdict) ------\n// These are unexported: only module 7's verdict executor reaches them, after\n// the I8 guards. They write only the meta bit / suspension flag — never money.\n\nfunc setMetaBit(c *Court, claimID uint64, by address) {\n\tcm := ensureMod(c)\n\tclm := ensureClaimMod(c, cm, claimID)\n\tclm.meta = true\n\tclm.executedAt = heightNow()\n\tclm.executedAtTime = nowTime()\n\tclm.appendLog(by, \"meta-hide\", \"\")\n\temitModAct(c.id, claimID, \"meta-hide\", by)\n}\n\nfunc clearMetaBit(c *Court, claimID uint64, by address) {\n\tcm := ensureMod(c)\n\tclm := lookupClaimMod(cm, claimID)\n\tif clm == nil || !clm.meta {\n\t\treturn // idempotent no-op\n\t}\n\tclm.meta = false\n\tclm.appendLog(by, \"meta-clear\", \"\")\n\temitModAct(c.id, claimID, \"meta-clear\", by)\n}\n\n// clearCourtBitByMeta clears the courtMod bit via a meta unhide verdict. It\n// stamps executedAt, which arms the I8 re-hide cooldown — that is what stops\n// the losing set re-hiding in the next block for free. It does NOT stamp a\n// \"by a third party\" marker: that refusal was removed deliberately (see\n// verbUnhide in meta.gno), and the stamps for it are gone (v0.16).\nfunc clearCourtBitByMeta(c *Court, claimID uint64, by address) {\n\tcm := ensureMod(c)\n\tclm := lookupClaimMod(cm, claimID)\n\tif clm == nil || !clm.court {\n\t\treturn\n\t}\n\tclm.court = false\n\tclm.executedAt = heightNow()\n\tclm.executedAtTime = nowTime()\n\tclm.appendLog(by, \"meta-unhide\", \"\")\n\temitModAct(c.id, claimID, \"meta-unhide\", by)\n}\n\nfunc suspendSet(c *Court, by address) {\n\tcm := ensureMod(c)\n\tcm.suspended = true\n\tcm.suspendedAt = heightNow()\n\tcm.suspendedSetID = currentSetID(cm) // remember WHICH membership was judged\n\tcm.suspendedM = cm.m                 // ...and the decision rule it was judged under\n\temitModAct(c.id, 0, \"suspend\", by)\n}\n\n// currentSetID is a canonical identity for a moderator set's MEMBERSHIP: its\n// sorted members, and nothing else. Sets are stored canonically, so equal\n// memberships produce equal IDs.\n//\n// The THRESHOLD is deliberately not part of this. It used to be, as an `m:`\n// prefix, and folding the two into one string meant the suspension rule could\n// only compare them together — which let a judged set loosen its own threshold\n// and walk out of its suspension. installModSet weighs the two separately now,\n// and both callers of this function (suspendSet, installModSet) are that rule.\nfunc currentSetID(cm *courtMod) string {\n\ts := \"\"\n\tcm.members.Iterate(\"\", \"\", func(k string, _ any) bool {\n\t\ts += k + \",\"\n\t\treturn false\n\t})\n\treturn s\n}\n\nfunc unsuspendSet(c *Court, by address) {\n\tcm := ensureMod(c)\n\tcm.suspended = false\n\temitModAct(c.id, 0, \"unsuspend\", by)\n}\n\n// ---- reads (render + tests; never read by a money path) -------------------\n\n// HiddenFromListing reports whether a claim is hidden from ordinary discovery\n// surfaces — the union of all three bits. Strips (module 3) override this for\n// policeable claims; this is the plain browse-listing predicate.\nfunc HiddenFromListing(courtSlug string, claimID uint64) bool {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn false\n\t}\n\tclm := lookupClaimMod(c.mod, claimID)\n\tif clm == nil {\n\t\treturn false\n\t}\n\treturn clm.court || clm.meta || clm.global\n}\n\n// TextRedacted reports whether a claim's user text must be withheld on every\n// surface (global hide or purge).\nfunc TextRedacted(courtSlug string, claimID uint64) bool {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn false\n\t}\n\tclm := lookupClaimMod(c.mod, claimID)\n\tif clm == nil {\n\t\treturn false\n\t}\n\treturn clm.global || clm.purged\n}\n\n// ClaimPurged / ClaimTombstone expose the purge state for render.\nfunc ClaimPurged(courtSlug string, claimID uint64) bool {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn false\n\t}\n\tclm := lookupClaimMod(c.mod, claimID)\n\treturn clm != nil \u0026\u0026 clm.purged\n}\n\n// PurgeModLogRow tombstones ONE moderation-log row's text, addressed by its\n// stable rowID. m-of-n, irreversible, unappealable — the same terms as\n// PurgeClaim, because it is the same power (MODERATION.md I11).\n//\n// It exists because the moderation log is itself a text surface, and until now\n// the only power that removes text could not reach it. A row's `reason` is FREE\n// TEXT a moderator wrote: it can carry the very thing a purge was called in to\n// remove — a name, a slur, a link — and \"we removed the claim\" is no answer\n// when the removal notice repeats what it removed. The row itself stays, with\n// its height, actor and act code, so the audit trail still shows that something\n// happened and who did it; only the prose goes.\n//\n// renderModLog has honoured modAct.purged since it was written. Nothing set it,\n// which is the inverse of the v0.16 sweep's complaint: state that LOOKS\n// load-bearing but is never WRITTEN is the same hazard as one never read.\nfunc PurgeModLogRow(cur realm, courtSlug string, claimID, rowID uint64, categoryCode string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\td := ensureGlobalDAO()\n\tif !d.members.Has(who.String()) {\n\t\tpanic(\"kourtv2: only a global DAO member may purge\")\n\t}\n\tmustCategoryCode(categoryCode)\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\tclm := lookupClaimMod(cm, claimID)\n\tif clm == nil {\n\t\tpanic(\"kourtv2: that claim has no moderation log\")\n\t}\n\t// Resolve the row BEFORE the vote, so an unreachable id is refused on the\n\t// first approval rather than after m-of-n has been spent on it.\n\tidx := -1\n\tfor i := range clm.log {\n\t\tif clm.log[i].rowID == rowID {\n\t\t\tidx = i\n\t\t\tbreak\n\t\t}\n\t}\n\tif idx \u003c 0 {\n\t\tpanic(\"kourtv2: no such moderation-log row\")\n\t}\n\tif clm.log[idx].purged {\n\t\treturn // already tombstoned; a second purge is a no-op, not an error\n\t}\n\tkey := \"purgerow:\" + c.id + \":\" + strconv.FormatUint(claimID, 10) + \":\" +\n\t\tstrconv.FormatUint(rowID, 10)\n\tfire, code := approveAction(d.pending, key, who, categoryCode, d.purgeM)\n\tif !fire {\n\t\treturn\n\t}\n\tclm.log[idx].purged = true\n\tclm.log[idx].reason = \"\" // gone from state, not merely hidden at render\n\tclm.appendLog(who, \"purgerow:\"+code, \"\")\n\temitPurge(c.id, claimID, code, who)\n}\n\n// PendingApproval reports how many approvals an action key has collected and\n// the height its window closes. A deadline nobody can see is a trap: a set that\n// gathered signatures over six days needs to know it has one left.\n//\n// Non-allocating, and reports an already-expired entry as absent — the same\n// answer approveAction will act on, so the read cannot disagree with the write\n// path (the v0.19 rule). courtSlug \"\" reads the global DAO's tree.\n// THE DEADLINE IS A DATE, WITH ITS HEIGHT BESIDE IT — the pairing ClaimTimeline\n// publishes, and the reason this signature gained a third value rather than\n// quietly changing what the second one measures. expiresAt is unix seconds and is\n// what the write path actually gates on; expiresAtHeight is the reference, and is\n// all a caller gets for an entry opened before the stamps existed (expiresAt 0).\n// A caller distinguishing \"nothing pending\" from \"no stamp\" reads approvals: it\n// is 0 only in the first case.\nfunc PendingApproval(courtSlug, actionKey string) (approvals int, expiresAt, expiresAtHeight int64) {\n\tvar pending *bptree.BPTree\n\tif courtSlug == \"\" {\n\t\tif globalDAO == nil {\n\t\t\treturn 0, 0, 0\n\t\t}\n\t\tpending = globalDAO.pending\n\t} else {\n\t\tc := mustCourt(courtSlug)\n\t\tif c.mod == nil {\n\t\t\treturn 0, 0, 0\n\t\t}\n\t\tpending = c.mod.pending\n\t}\n\tv := pending.Get(actionKey)\n\tif v == nil {\n\t\treturn 0, 0, 0\n\t}\n\ta := v.(*approval)\n\tif approvalStale(a) {\n\t\treturn 0, 0, 0\n\t}\n\treturn a.approvers.Size(),\n\t\tdeadlineTime(a.openedAtTime, blocksToSecs(pendingTTLBlocks)),\n\t\ta.openedAt + pendingTTLBlocks\n}\n\n// CourtSuspended reports whether a court's moderator set is suspended (masked).\nfunc CourtSuspended(courtSlug string) bool {\n\tc := mustCourt(courtSlug)\n\treturn c.mod != nil \u0026\u0026 c.mod.suspended\n}\n\n// IsCourtMod reports set membership (render shows the set; tests assert it).\n//\n// A court's moderation state is created LAZILY — ensureMod bootstraps the\n// admin as a 1-of-1 on the first act — so before any act there is no object to\n// read. Answering \"no\" then was wrong, and wrong in the direction that\n// misleads: HideItem(admin) SUCCEEDS on a court whose IsCourtMod(admin) said\n// false, so a UI would tell the one person who can moderate that they cannot.\n// The read now reports the set the write path will actually act on. Found by\n// the moderation txtar, which is the first thing to ask this question of a\n// court that had not been moderated yet.\nfunc IsCourtMod(courtSlug string, who address) bool {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn who == c.admin\n\t}\n\treturn c.mod.members.Has(who.String())\n}\n\n// ModThreshold returns a court's m and n — 1-of-1 before the set materialises,\n// for the same reason IsCourtMod does.\nfunc ModThreshold(courtSlug string) (m, n int) {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn 1, 1\n\t}\n\treturn c.mod.m, c.mod.n\n}\n\n// GlobalModCount is the size of the global DAO set.\nfunc GlobalModCount() int {\n\tif globalDAO == nil {\n\t\treturn 0\n\t}\n\treturn globalDAO.n\n}\n\n// ModLogLen is how many log rows a claim's moderation has accrued.\nfunc ModLogLen(courtSlug string, claimID uint64) int {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn 0\n\t}\n\tclm := lookupClaimMod(c.mod, claimID)\n\tif clm == nil {\n\t\treturn 0\n\t}\n\treturn len(clm.log)\n}\n\n// ---- events (metadata + category/act code only; never user text) ----------\n\nfunc emitModAct(slug string, claimID uint64, code string, actor address) {\n\tchain.Emit(modActEvent,\n\t\t\"court\", slug,\n\t\t\"claim\", strconv.FormatUint(claimID, 10),\n\t\t\"act\", code,\n\t\t\"by\", actor.String(),\n\t\t\"height\", eventHeight(),\n\t)\n}\n\nfunc emitGlobalAct(slug string, claimID uint64, code string, actor address) {\n\tchain.Emit(globalActEvent,\n\t\t\"court\", slug,\n\t\t\"claim\", strconv.FormatUint(claimID, 10),\n\t\t\"act\", code,\n\t\t\"by\", actor.String(),\n\t\t\"height\", eventHeight(),\n\t)\n}\n\nfunc emitPurge(slug string, claimID uint64, categoryCode string, actor address) {\n\tchain.Emit(purgeEvent,\n\t\t\"court\", slug,\n\t\t\"claim\", strconv.FormatUint(claimID, 10),\n\t\t// Caller-supplied and bounded by LENGTH ONLY — see mustCategoryCode, which\n\t\t// says why there is no alphabet. It said \"a statutory category enum, not\n\t\t// user text\", which was wrong: nothing makes it an enum. The section header\n\t\t// above is still accurate, because a category code is what it allows.\n\t\t\"code\", categoryCode,\n\t\t\"by\", actor.String(),\n\t\t\"height\", eventHeight(),\n\t)\n}\n"},{"name":"modrender.gno","body":"// Moderation-aware render surfaces (MODERATION.md §5, §3.2) and the front\n// page's two orderings.\n//\n// Kept beside render.gno rather than inside it so the claim page — the product\n// — stays one readable file; render.gno calls into the helpers here at the few\n// points where moderation changes what a reader sees:\n//\n//   - ordinary LISTINGS drop hidden rows (the only place a hide bit subtracts);\n//   - DEEP LINKS always render, with a banner naming the authority;\n//   - the GLOBAL bit redacts user text on EVERY surface but still shows IDs,\n//     state, deadlines and money, so a participant can always act;\n//   - PURGED rows render a category-code tombstone in place of text;\n//   - the POLICING STRIPS ignore courtMod/meta bits entirely.\n//\n// Front-page ordering: GNOT-burn descending by default. Burned GNOT is the one\n// signal an attacker cannot manufacture (you cannot fake burning real money),\n// so ranking on it needs no gameable activity/vote score. Creation-newest-first\n// is the alternative. Both read monotone secondary indexes and are paginated,\n// so page cost never grows with the court count.\n\npackage kourtv2\n\nimport (\n\t\"math/overflow\"\n\t\"strconv\"\n\t\"strings\"\n\n\tbptree \"gno.land/p/nt/bptree/v0\"\n\tsanitize \"gno.land/p/nt/markdown/sanitize/v0\"\n)\n\n// burnIdx orders courts by cumulative GNOT burned, DESCENDING: the key inverts\n// the burn total so ascending iteration yields the biggest first. createdIdx\n// orders by creation height, and is reverse-iterated for newest-first.\nvar (\n\tburnIdx    = bptree.NewBPTree32()\n\tcreatedIdx = bptree.NewBPTree32()\n\tcourtSeq   uint64\n)\n\n// beInv encodes a descending key: larger values sort earlier.\nfunc beInv(v int64) string {\n\tif v \u003c 0 {\n\t\tv = 0\n\t}\n\treturn beClaimKey(^uint64(v))\n}\n\n// reindexBurn adds `spent` to a court's burn total and re-keys its index row.\n// Called from Buy; never panics, never reads money state back.\nfunc reindexBurn(c *Court, spent int64) {\n\t// Checked add with a SILENT skip — deliberately not mustAdd. This runs\n\t// inside Buy, the platform's only GNOT on-ramp, so a panic here would brick\n\t// buying on every court; an unreachable overflow must degrade the index,\n\t// never the money path. (Same discipline as accrueFranchise.)\n\tsum, ok := overflow.Add64(c.burnedGNOT, spent)\n\tif !ok {\n\t\treturn\n\t}\n\tif c.burnedGNOT \u003e 0 || c.burnKey != \"\" {\n\t\tburnIdx.Remove(c.burnKey)\n\t}\n\tc.burnedGNOT = sum\n\tc.burnKey = beInv(c.burnedGNOT) + c.id\n\tburnIdx.Set(c.burnKey, c.id)\n\t// Checkpoint the new total for the court page's chart. AFTER the total\n\t// moves, and change-only, the same order recordPools follows. Buys inside\n\t// one epoch coalesce, so a busy hour costs one row and not one per buy.\n\trecordBurn(c)\n}\n\n// indexNewCourt registers a court in both directory indexes at creation.\nfunc indexNewCourt(c *Court) {\n\tcourtSeq++\n\tc.createdSeq = courtSeq\n\tcreatedIdx.Set(beClaimKey(c.createdSeq), c.id)\n\tc.burnKey = beInv(0) + c.id\n\tburnIdx.Set(c.burnKey, c.id)\n}\n\n// listedPage returns up to limit listed-tier slugs in the requested order,\n// skipping offset. order \"new\" is creation-newest-first; anything else is the\n// default, GNOT-burn descending. Hidden-tier courts never appear.\nfunc listedPage(order string, offset, limit int) []string {\n\tout := []string{}\n\tif limit \u003c= 0 {\n\t\treturn out\n\t}\n\tskipped := 0\n\ttake := func(_ string, v any) bool {\n\t\tslug, ok := v.(string)\n\t\tif !ok || !Exists(slug) {\n\t\t\treturn false\n\t\t}\n\t\tc := mustCourt(slug)\n\t\tif c.tier == tierHidden {\n\t\t\treturn false\n\t\t}\n\t\tif skipped \u003c offset {\n\t\t\tskipped++\n\t\t\treturn false\n\t\t}\n\t\tout = append(out, slug)\n\t\treturn len(out) \u003e= limit\n\t}\n\tif order == \"new\" {\n\t\tcreatedIdx.ReverseIterate(\"\", \"\", take)\n\t} else {\n\t\tburnIdx.Iterate(\"\", \"\", take)\n\t}\n\treturn out\n}\n\n// ListedCourtsBy is the paginated front-page read (\"burn\" | \"new\").\nfunc ListedCourtsBy(order string, offset, limit int) []string {\n\treturn listedPage(order, offset, limit)\n}\n\n// CourtBurnedGNOT is the cumulative GNOT burned into a court's curve.\nfunc CourtBurnedGNOT(courtSlug string) int64 { return mustCourt(courtSlug).burnedGNOT }\n\n// ---- text gates used by every render path ---------------------------------\n\n// claimTitleFor is THE single place a claim's title becomes display text. A\n// purged row shows its statutory category code; a globally-redacted row shows\n// nothing at all. Everything else is sanitized.\nfunc claimTitleFor(c *Court, cs *claimState) string {\n\tif c.mod != nil {\n\t\tif clm := lookupClaimMod(c.mod, cs.id); clm != nil {\n\t\t\tif clm.purged {\n\t\t\t\treturn \"[removed · \" + sanitize.InlineText(clm.tombstone) + \"]\"\n\t\t\t}\n\t\t\tif clm.global {\n\t\t\t\treturn \"[text withheld]\"\n\t\t\t}\n\t\t}\n\t}\n\treturn sanitize.InlineText(cs.title)\n}\n\n// claimBodyVisible is the MODERATION gate alone: the body a given reader is\n// allowed to see, unsanitised. A purged claim answers with NOTHING rather than\n// with its tombstone — the title already carries \"[removed · \u003ccategory\u003e]\", and\n// repeating it here would be noise where the point is absence.\n//\n// EVERY CALLER MUST SANITISE FOR ITS OWN OUTPUT CONTEXT, and they do not agree on\n// which helper, which is exactly why this returns raw. sanitize/v0's own rule is\n// \"sanitise once, at the point of output\", and the two outputs are different\n// documents:\n//\n//   the ClaimBody read  -\u003e sanitize.Block. The overlay consumes it and does its\n//                          own escaping; a `\u003e ` on every line would be a\n//                          markdown artefact in a place with no markdown.\n//   Render's markdown   -\u003e sanitize.Blockquote. See below.\nfunc claimBodyVisible(c *Court, cs *claimState) string {\n\tif cs.body == \"\" {\n\t\treturn \"\"\n\t}\n\tif c.mod != nil {\n\t\tif clm := lookupClaimMod(c.mod, cs.id); clm != nil {\n\t\t\tif clm.purged || clm.global {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t}\n\t}\n\treturn cs.body\n}\n\n// claimMediaVisible is claimBodyVisible for the evidence a claim carries, and it\n// exists for the same reason: a purged or globally-redacted claim must not answer\n// with the thing the purge was for. Media is the surface most likely to carry\n// exactly that, so a raw accessor here would make the moderation layer decorative\n// in the one place it matters most.\nfunc claimMediaVisible(c *Court, cs *claimState) []mediaItem {\n\tif len(cs.media) == 0 {\n\t\treturn nil\n\t}\n\tif c.mod != nil {\n\t\tif clm := lookupClaimMod(c.mod, cs.id); clm != nil {\n\t\t\tif clm.purged || clm.global {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\treturn cs.media\n}\n\n// claimBodyFor is the body as the ClaimBody read answers it: gated, then\n// sanitize.Block — the STRICT variant, which escapes the doc-spoof markers (ATX\n// headings, blockquote markers, list markers, thematic breaks, setext\n// underlines) that sanitize.BlockRich deliberately preserves. Strict is right\n// here: the body is the only multi-LINE string a stranger writes on a claim page,\n// and every other user string on it is inline and cannot open a block at all.\n//\n// Block, not InlineText, because a body is multi-line by design and InlineText\n// folds it to one — which would silently destroy the paragraphs the field is for.\nfunc claimBodyFor(c *Court, cs *claimState) string {\n\tbody := claimBodyVisible(c, cs)\n\tif body == \"\" {\n\t\treturn \"\"\n\t}\n\treturn sanitize.Block(body)\n}\n\n// claimBodyQuoted is the body for the MARKDOWN surface, and it is quoted rather\n// than merely escaped. Block escapes CommonMark §4.6 HTML block types 1-5 —\n// \u003cscript\u003e, \u003cpre\u003e, \u003cstyle\u003e, \u003ctextarea\u003e — but by its own documentation it does NOT\n// escape types 6 and 7: \u003cdiv\u003e, \u003ctable\u003e, \u003cform\u003e, arbitrary \u003cfoo\u003e. Those it only\n// CONTAINS, with a trailing blank line that closes the block before the realm's\n// own chrome resumes. A test written against this realm's integration proved it:\n// a body of \"\u003cdiv\u003e\" reached the page at line start, and the sections below it\n// survived only because the envelope closed the block.\n//\n// Contained is not the same as absent. gnoweb runs no HTML sanitiser after the\n// realm, so a `\u003cform\u003e` in a body would render a form on a page people stake money\n// on, and a `\u003ctable\u003e` would forge a data table under a headline. Blockquote is\n// Block plus a `\u003e ` on every line, so whatever HTML still opens does so INSIDE a\n// quotation that closes at its own blank line — and that is also what the body\n// honestly is: the author's statement, quoted, not the court's own words.\nfunc claimBodyQuoted(c *Court, cs *claimState) string {\n\tbody := claimBodyVisible(c, cs)\n\tif body == \"\" {\n\t\treturn \"\"\n\t}\n\treturn sanitize.Blockquote(body)\n}\n\n// platformName / platformTicker are the product's identity.\nconst (\n\tplatformName   = \"Kourt\"\n\tplatformTicker = \"KOURT\"\n)\n\n// qualifiedSymbol renders a court coin as KOURT:SYMBOL. A BARE court symbol is\n// only ever shown inside that court's own page: court coins are non-fungible and\n// backed by separate one-way curves, so an unqualified ticker in a directory or\n// a wallet-style list invites reading one court's coin as another's — or as the\n// platform token.\nfunc qualifiedSymbol(c *Court) string {\n\treturn platformTicker + \":\" + sanitize.InlineText(c.coin.Symbol())\n}\n\n// courtNameFor gates a COURT's name the way claimTitleFor gates a claim title.\n// A whole-court purge is a legal-hold act, and §2 lists court names in the\n// user-text inventory purge must reach — rendering the raw name anywhere would\n// leave the offending text on the directory and on every page of that court.\nfunc courtNameFor(c *Court) string {\n\tif c.mod != nil \u0026\u0026 c.mod.courtPurged {\n\t\treturn \"[removed · \" + sanitize.InlineText(c.mod.courtTombstone) + \"]\"\n\t}\n\treturn sanitize.InlineText(c.name)\n}\n\n// courtDescFor is the description behind the same gate as the name: a purged\n// court answers with nothing rather than with its tombstone, because the NAME\n// already carries \"[removed · \u003ccode\u003e]\" and repeating it under itself is noise\n// where the point is absence. InlineText, not Block — the field is a single\n// paragraph by construction (mustCourtDesc refuses newlines), and it renders\n// inline beside the name in list rows.\nfunc courtDescFor(c *Court) string {\n\tif c.desc == \"\" {\n\t\treturn \"\"\n\t}\n\tif c.mod != nil \u0026\u0026 c.mod.courtPurged {\n\t\treturn \"\"\n\t}\n\treturn sanitize.InlineText(c.desc)\n}\n\n// hideBanner names the authority hiding a claim, for deep links (which always\n// render). Empty when nothing hides it.\nfunc hideBanner(c *Court, cs *claimState) string {\n\tif c.mod == nil {\n\t\treturn \"\"\n\t}\n\tclm := lookupClaimMod(c.mod, cs.id)\n\tif clm == nil {\n\t\treturn \"\"\n\t}\n\tswitch {\n\tcase clm.purged:\n\t\treturn \"\u003e **Removed on legal grounds** (\" + sanitize.InlineText(clm.tombstone) +\n\t\t\t\"). The claim's lifecycle is unaffected: stakes, verdict and refunds proceed.\\n\\n\"\n\tcase clm.global:\n\t\treturn \"\u003e **Text withheld pending review.** The claim's id, its state, its deadlines and every balance are still shown, so you can always act on a position.\\n\\n\"\n\tcase clm.meta:\n\t\treturn \"\u003e **Hidden by the review court.** Listed nowhere, reachable here; the lifecycle is unaffected.\\n\\n\"\n\tcase clm.court:\n\t\treturn \"\u003e **Hidden by this court's moderators.** Listed nowhere, reachable here; the lifecycle is unaffected.\\n\\n\"\n\t}\n\treturn \"\"\n}\n\n// ---- the policing strip + pending list pages ------------------------------\n\n// One copy of each, so the heading the page prints and the heading its own test\n// looks for cannot drift apart.\nconst (\n\tstripHeading = \"Still flaggable\"\n\tstripBlurb   = \"Answered claims that can still be flagged as spam — nearest deadline first. A claim leaves this list when its rewards are opened, not when it settles, so a settled claim stays here while it can still be flagged. This list ignores moderation: a claim hidden from the lists above still appears here.\"\n)\n\nfunc writeStrip(b *strings.Builder, slug string) {\n\tc := mustCourt(slug)\n\tn := StripSize(slug)\n\tif n == 0 {\n\t\treturn\n\t}\n\t// WHAT THIS LIST IS, said in the words of the rule that builds it. It used to\n\t// call itself \"Needs review — Claims that are answered and not yet settled\",\n\t// and both halves misled.\n\t//\n\t// NOTHING HERE NEEDS A MODERATOR. The name says a review is owed; the list is\n\t// the flaggable window — claims anyone may still flag as spam. Asked directly:\n\t// why does it need review?\n\t//\n\t// AND \"NOT YET SETTLED\" WAS FALSE, provably, off the realm's own reads. A claim\n\t// enters when it is answered and leaves at stripExit, which fires on\n\t// rewardsOpened / provClosed / dead-closed — settling is not one of them. On\n\t// covid that made eleven of fourteen rows claims whose ClaimStatus reads\n\t// \"settled YES\", listed under a sentence saying they were not settled, and\n\t// duplicated against the court page's Recently settled. The duplication is\n\t// correct: one list says decided, the other says still flaggable, and both are\n\t// true at once. Only the sentence was wrong.\n\tb.WriteString(\"## \" + stripHeading + \"\\n\\n\" + stripBlurb + \"\\n\\n\")\n\tids := StripPage(slug, 0, renderPageSize)\n\tfor _, id := range ids {\n\t\tcs := mustClaim(c, id)\n\t\tb.WriteString(\"- [\" + claimTitleFor(c, cs) + \"](/r/kourt/kourtv2:\" + slug + \"/\" +\n\t\t\tstrconv.FormatUint(id, 10) + \") — settles at block \" +\n\t\t\tstrconv.FormatInt(cs.answerHeight+settleDelay, 10) + \"\\n\")\n\t}\n\tif n \u003e len(ids) {\n\t\tb.WriteString(\"- _…and \" + strconv.Itoa(n-len(ids)) + \" more_\\n\")\n\t}\n\tb.WriteString(\"\\n\")\n}\n\nfunc writePending(b *strings.Builder, slug string) {\n\tc := mustCourt(slug)\n\tn := PendingSize(slug)\n\tif n == 0 {\n\t\treturn\n\t}\n\tb.WriteString(\"## Awaiting an answer\\n\\nSeeded and appeal claims that no one has answered yet.\\n\\n\")\n\tids := PendingPage(slug, 0, renderPageSize)\n\tfor _, id := range ids {\n\t\tcs := mustClaim(c, id)\n\t\ttag := \"\"\n\t\tif cs.seeded {\n\t\t\ttag = \" — seeded by a moderator\"\n\t\t}\n\t\tb.WriteString(\"- [\" + claimTitleFor(c, cs) + \"](/r/kourt/kourtv2:\" + slug + \"/\" +\n\t\t\tstrconv.FormatUint(id, 10) + \")\" + tag + \"\\n\")\n\t}\n\tif n \u003e len(ids) {\n\t\tb.WriteString(\"- _…and \" + strconv.Itoa(n-len(ids)) + \" more_\\n\")\n\t}\n\tb.WriteString(\"\\n\")\n}\n\n// writeAppeal renders a meta claim's parsed instruction — the verb, the target,\n// and for a setmods appeal the exact moderator set it would install.\n//\n// This closes the amplifier on the setmods bait-and-switch: the ballot is a\n// claim TITLE, so without decoding it here the electorate votes on an opaque\n// string and cannot see which addresses they are seating. It renders the STORED\n// parse, which survives a purge, so a redacted appeal still shows verb+target\n// (realm-validated, category-code-class strings) rather than blinding its own\n// voters.\nfunc writeAppeal(b *strings.Builder, c *Court, cs *claimState) {\n\tif c.id != metaSlug {\n\t\treturn\n\t}\n\tverb, court, target, valid := AppealParse(cs.id)\n\tif !valid {\n\t\tb.WriteString(\"\u003e _Not a valid review request — this is an ordinary claim on the review court._\\n\\n\")\n\t\treturn\n\t}\n\tb.WriteString(\"## Review request\\n\\n\")\n\tb.WriteString(\"- action: **\" + sanitize.InlineText(verb) + \"** — \" +\n\t\tappealVerbGloss(verb) + \"\\n\")\n\tb.WriteString(\"- court: \" + sanitize.InlineText(court) + \"\\n\")\n\t// WHAT SILENCE DOES, which is the opposite thing for the two kinds of verb\n\t// and was on no surface. A restorative verb may carry on the undisputed\n\t// route, so a voter who does nothing lets it through; an aggressive one is\n\t// refused unless a quorate round decided it against real opposition, so a\n\t// voter who does nothing blocks it. Reading only the verb code, a voter\n\t// cannot tell which of those their abstention is.\n\tif verbIsAggressive(verb) {\n\t\tb.WriteString(\"- this is an **aggressive** act: it can only carry on a \" +\n\t\t\t\"contested vote that real opposition turned out for. Silence \" +\n\t\t\t\"defeats it.\\n\")\n\t} else {\n\t\tb.WriteString(\"- this is a **restorative** act: it undoes a restriction, so \" +\n\t\t\t\"it can carry undisputed. Silence lets it through.\\n\")\n\t}\n\tif verb == verbSetmods {\n\t\tb.WriteString(\"- installs candidate set #\" + strconv.FormatUint(target, 10) + \":\\n\")\n\t\tif !Exists(court) {\n\t\t\tb.WriteString(\"  - _that court does not exist_\\n\\n\")\n\t\t\treturn\n\t\t}\n\t\ttc := mustCourt(court)\n\t\tif tc.mod == nil || tc.mod.candidates == nil ||\n\t\t\ttc.mod.candidates.Get(beClaimKey(target)) == nil {\n\t\t\t// An unregistered id is exactly the bait-and-switch shape: say so\n\t\t\t// loudly rather than rendering a blank.\n\t\t\tb.WriteString(\"  - **this candidate set does not exist yet — do not approve**\\n\\n\")\n\t\t\treturn\n\t\t}\n\t\tcand := tc.mod.candidates.Get(beClaimKey(target)).(*modCandidate)\n\t\tfor _, a := range cand.members {\n\t\t\tb.WriteString(\"  - \" + sanitize.InlineText(a.String()) + \"\\n\")\n\t\t}\n\t\tb.WriteString(\"  - threshold: \" + strconv.Itoa(cand.m) + \" of \" +\n\t\t\tstrconv.Itoa(len(cand.members)) + \" must agree\\n\")\n\t} else if target != 0 {\n\t\tb.WriteString(\"- claim: \" + strconv.FormatUint(target, 10) + \"\\n\")\n\t}\n\tb.WriteString(\"\\n\")\n}\n\n// ---- the moderation log route (\u003cslug\u003e/mod) --------------------------------\n\n// modActLine formats one log row, and is the ONLY place a log row becomes text.\n//\n// ONE COPY, TWO LOGS. The court-level log (boardmod.gno) and the per-claim log\n// render identically apart from the label, and the second copy of these four\n// lines immediately made two existing mutation-corpus anchors ambiguous — the\n// corpus caught the duplication before a divergence could. `kind` is the only\n// difference, because the two logs have SEPARATE row-id sequences and each has\n// its own purge verb, so a reader has to be able to tell which id they are\n// looking at.\n//\n// The row id is PRINTED because it is the handle PurgeModLogRow and\n// PurgeCourtLogRow take (I11). A power addressed by an id nobody can see is a\n// power nobody can use.\nfunc modActLine(a modAct, kind string) string {\n\tline := \"- \" + kind + \" \" + strconv.FormatUint(a.rowID, 10) +\n\t\t\" · block \" + strconv.FormatInt(a.height, 10) + \" · \" +\n\t\tsanitize.InlineText(a.code) + \" · by \" + sanitize.InlineText(a.actor.String())\n\tswitch {\n\tcase a.purged:\n\t\tline += \" — _[reason removed on legal grounds]_\"\n\tcase a.reason != \"\":\n\t\tline += \" — \" + sanitize.InlineText(a.reason)\n\t}\n\treturn line\n}\n\nfunc renderModLog(slug string) string {\n\tif !Exists(slug) {\n\t\treturn \"## Not found\\nNo court by that slug.\"\n\t}\n\tc := mustCourt(slug)\n\tb := strings.Builder{}\n\tb.WriteString(\"# Moderation log — \" + courtNameFor(c) + \"\\n\\n\")\n\t// WHAT IT ACTUALLY COVERS. This said \"every moderation act on this court\",\n\t// and eleven do not appear: the ten folder verbs and court-desc emit a\n\t// ModAct event and never call appendLog. A demo court with three folders and\n\t// five filings read \"No moderation acts yet\" underneath the promise.\n\t//\n\t// The sentence is narrowed rather than the acts logged, because logging them\n\t// changes what this log IS — cm.log is where a board sanction lands precisely\n\t// because it has no claim to hang off, and a busy curator would push\n\t// sanctions off the first page of a bounded render. Recorded as a ruling in\n\t// docs/CLAIM_BOARDS.md §19 so it is a decision and not a default.\n\t//\n\t// It names where curation IS visible, because §18.2 made the filing tree\n\t// renderable: a reader who can now see that somebody organised a contested\n\t// docket should be told plainly where the record of that organising is, and\n\t// is not.\n\tb.WriteString(\"Every moderation act on this court's CLAIMS and COMMENTS is recorded here: who acted, what they did, and when. Moderation controls what is listed; it never moves a coin or changes a verdict.\\n\\n\")\n\tb.WriteString(\"[Who holds these seats, and how to challenge them](/r/kourt/kourtv2:\" +\n\t\tsanitize.InlineText(slug) + \"/election)\\n\\n\")\n\tb.WriteString(\"Curation — which claims a moderator filed under which heading — is \" +\n\t\t\"not in this list. The tree it produced is on the [court page](/r/kourt/kourtv2:\" + sanitize.InlineText(slug) + \"); the acts themselves are in \" +\n\t\t\"the chain's event stream.\\n\\n\")\n\tif c.mod == nil {\n\t\t// Lazily created, so there is no object yet — but the set is not empty:\n\t\t// the admin is the 1-of-1 until an act, an appointment or an election\n\t\t// materialises one. Saying only \"no acts yet\" hid who could act.\n\t\tb.WriteString(\"## Moderators\\n\\n1-of-1 — \" + c.admin.String() +\n\t\t\t\" (the court's creator, until an election or an appointment changes it)\\n\\n\")\n\t\tb.WriteString(\"_No moderation acts yet._\\n\")\n\t\treturn b.String()\n\t}\n\tcm := c.mod\n\tif cm.courtPurged {\n\t\tb.WriteString(\"\u003e **This court was removed on legal grounds** (\" +\n\t\t\tsanitize.InlineText(cm.courtTombstone) + \"). No new claims may be opened; existing claims settle and pay out normally.\\n\\n\")\n\t}\n\tb.WriteString(\"## Moderators\\n\\n\")\n\tif cm.n == 0 {\n\t\tb.WriteString(\"_None — the set is empty; the court's holders can elect one._\\n\\n\")\n\t} else {\n\t\tb.WriteString(\"- \" + strconv.Itoa(cm.m) + \" of \" + strconv.Itoa(cm.n) +\n\t\t\t\" must agree for a moderation act\\n\")\n\t\t// AND THE BOARD THRESHOLD, WHICH IS NOT THE SAME NUMBER. Board acts fire\n\t\t// at speechM = min(n, max(2, m)), so a 1-of-3 set needs two approvals to\n\t\t// touch anyone's speech. This page printed only `m of n` and therefore\n\t\t// stated the wrong rule for every court where the two differ; SpeechThreshold\n\t\t// existed with no caller at all.\n\t\tif sm := speechM(cm); sm != cm.m {\n\t\t\tb.WriteString(\"- \" + strconv.Itoa(sm) + \" of \" + strconv.Itoa(cm.n) +\n\t\t\t\t\" for a **board** act — hiding a comment, slashing standing, \" +\n\t\t\t\t\"freezing an address — because speech takes a wider agreement \" +\n\t\t\t\t\"than the set's own threshold\\n\")\n\t\t}\n\t\tif cm.suspended {\n\t\t\t// WHAT SUSPENSION ACTUALLY DOES. This line used to say \"their hides\n\t\t\t// are masked and they cannot act\". The second half is true; the first\n\t\t\t// is not, and no visibility predicate in the realm consults\n\t\t\t// cm.suspended — HiddenFromListing is `court || meta || global` and\n\t\t\t// boardMark reads the row's own bits. A page that promises a masking\n\t\t\t// the code does not perform tells a reader their content is back when\n\t\t\t// it is not. §7.4 asks for the masking; docs/CLAIM_BOARDS.md carries\n\t\t\t// the ruling on whether to build it.\n\t\t\tb.WriteString(\"- **suspended** — they cannot take new moderation acts. \" +\n\t\t\t\t\"The hides and freezes already in place stand until cleared, and \" +\n\t\t\t\t\"any member may still lift their own\\n\")\n\t\t}\n\t\tif cm.installedByMeta {\n\t\t\tb.WriteString(\"- seated by the review court; may not seed claims until a local election\\n\")\n\t\t}\n\t\tb.WriteString(\"\\n\")\n\t}\n\t// In-flight proposals, with their deadlines. An m-of-n set that cannot see\n\t// what it has half-approved coordinates by rumour, and v0.25 gave every\n\t// unfired approval a 7-day expiry — a deadline nobody can see is a trap for\n\t// a set six days into gathering signatures.\n\t//\n\t// The action KEY, the count and the deadline render. The stored `reason` does\n\t// NOT, deliberately: it is free text one moderator wrote, and no m-of-n has\n\t// approved it yet. Rendering it would hand any single member of the set a\n\t// publication channel on the court's own page without the set's consent —\n\t// which is the same authority-without-agreement the m-of-n exists to prevent.\n\t// It appears in the log only once the act FIRES.\n\t//\n\t// Bounded on entries VISITED, like the act loop below: this is the only place\n\t// in the realm that walks `pending`, and an unbounded walk here would undo\n\t// the v0.21 lesson one function away from where it was learned.\n\tpendingRows := []string{}\n\tpendingSeen := 0\n\tpendingMore := false\n\tcm.pending.Iterate(\"\", \"\", func(key string, v any) bool {\n\t\tpendingSeen++\n\t\tif a, ok := v.(*approval); ok {\n\t\t\t// LIVE OR NOT IS approvalStale's ANSWER, not a second opinion. This\n\t\t\t// filter used to recompute the window in blocks while the write path\n\t\t\t// and every reader had moved to the stamp — so on a chain off 5s a\n\t\t\t// block the page could list an entry approveAction treats as stale,\n\t\t\t// or hide one that is still live. That is the reopen link's bug\n\t\t\t// (dispute.gno) on a different surface, and the fix is the same: one\n\t\t\t// predicate. `deadline` stays for the block it PRINTS beside the date.\n\t\t\tdeadline := a.openedAt + pendingTTLBlocks\n\t\t\tif !approvalStale(a) {\n\t\t\t\t// The DATE is what the realm gates on; the block is the\n\t\t\t\t// reference beside it (clock.gno). An entry opened before the\n\t\t\t\t// stamps has no date, and shows only the block it will lift at.\n\t\t\t\twhen := \" (block \" + strconv.FormatInt(deadline, 10) + \")\"\n\t\t\t\tif at := deadlineTime(a.openedAtTime, blocksToSecs(pendingTTLBlocks)); at != 0 {\n\t\t\t\t\twhen = \" \" + strconv.FormatInt(at, 10) + when\n\t\t\t\t}\n\t\t\t\tpendingRows = append(pendingRows,\n\t\t\t\t\t// InlineCode, not backticks around InlineText. A backslash\n\t\t\t\t\t// escape is INERT inside a CommonMark code span, so\n\t\t\t\t\t// InlineText's `\\`` would render the backslash literally and\n\t\t\t\t\t// the backtick would close the span early, spilling the rest\n\t\t\t\t\t// into the page. InlineCode is the helper for this position:\n\t\t\t\t\t// it sizes the fence to the content instead of escaping.\n\t\t\t\t\t//\n\t\t\t\t\t// NOT a live bug — every key here is verb:slug:id and mustSlug\n\t\t\t\t\t// bounds a slug to [a-z0-9-]{1,11}, so no key can hold a\n\t\t\t\t\t// backtick today. The invariant that makes the old code safe\n\t\t\t\t\t// lives in court.gno, three files from the thing it protects,\n\t\t\t\t\t// and a future key that carries a reason or a name would make\n\t\t\t\t\t// it exploitable with no failing test.\n\t\t\t\t\t\"- \"+sanitize.InlineCode(key)+\" — \"+strconv.Itoa(a.approvers.Size())+\n\t\t\t\t\t\t\" of \"+strconv.Itoa(cm.m)+\" approved, expires at\"+when)\n\t\t\t}\n\t\t}\n\t\tif len(pendingRows) \u003e= renderPageSize || pendingSeen \u003e= renderPageSize {\n\t\t\tpendingMore = true\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\tif len(pendingRows) \u003e 0 {\n\t\tb.WriteString(\"## Awaiting approval\\n\\nProposals this set has started and not yet carried. An approval expires if the set does not reach its threshold in time; the reason is withheld until the act carries.\\n\\n\")\n\t\tfor _, r := range pendingRows {\n\t\t\tb.WriteString(r + \"\\n\")\n\t\t}\n\t\tif pendingMore {\n\t\t\tb.WriteString(\"- _…more not shown_\\n\")\n\t\t}\n\t\tb.WriteString(\"\\n\")\n\t}\n\n\t// Newest acts first, one page (the N3 bound). ReverseIterate matters: claim\n\t// keys are big-endian ids, so a forward walk starts at the OLDEST moderated\n\t// claim — once those filled a page, no recent act was ever visible, which is\n\t// the opposite of what a moderation log is for.\n\t//\n\t// TWO bounds, not one. Stopping only when ROWS fill a page is a bound on\n\t// output, not on work: a claimMod carrying few or no acts costs a step and\n\t// buys no row, so a court with many lightly-moderated claims walks them all.\n\t// Nothing mints an act-less row today (ensureClaimMod runs only after the\n\t// m-of-n fires), but that is an invariant elsewhere in the file, and a\n\t// render should not silently depend on one. So the WALK is bounded too.\n\trows := []string{}\n\tvisited := 0\n\ttruncated := false\n\t// THE COURT-LEVEL LOG FIRST, and newest-first within it. A board sanction is\n\t// keyed to an ADDRESS, so it has no claim to hang off and lands in cm.log\n\t// (boardmod.gno). It is read first because those are the acts a reader is\n\t// least likely to find any other way: a claim's own page shows its hides, but\n\t// nothing shows a slash except this list.\n\t//\n\t// Rows are labelled \"court row\" because the two logs have SEPARATE\n\t// sequences — a court row and a claim row may share a number, and\n\t// PurgeCourtLogRow and PurgeModLogRow are different verbs.\n\tfor i := len(cm.log) - 1; i \u003e= 0 \u0026\u0026 len(rows) \u003c renderPageSize; i-- {\n\t\trows = append(rows, modActLine(cm.log[i], \"court row\"))\n\t}\n\tif len(rows) \u003e= renderPageSize {\n\t\ttruncated = true\n\t}\n\tcm.claims.ReverseIterate(\"\", \"\", func(_ string, v any) bool {\n\t\tif len(rows) \u003e= renderPageSize {\n\t\t\ttruncated = true\n\t\t\treturn true\n\t\t}\n\t\tclm, ok := v.(*claimMod)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\tvisited++\n\t\tfor i := len(clm.log) - 1; i \u003e= 0; i-- {\n\t\t\trows = append(rows, modActLine(clm.log[i], \"row\"))\n\t\t\tif len(rows) \u003e= renderPageSize {\n\t\t\t\ttruncated = true\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\tif visited \u003e= renderPageSize {\n\t\t\ttruncated = true\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\tb.WriteString(\"## Acts\\n\\n\")\n\tif len(rows) == 0 {\n\t\tb.WriteString(\"_No moderation acts yet._\\n\")\n\t\treturn b.String()\n\t}\n\tfor _, r := range rows {\n\t\tb.WriteString(r + \"\\n\")\n\t}\n\t// SAY SO when the page is not the whole log. A moderation log is an\n\t// accountability record, and one that silently shows a slice of itself\n\t// invites the reader to conclude that nothing else happened. The exact\n\t// remaining count is deliberately not computed — that would re-walk\n\t// everything this bound exists to avoid — but the claim count is O(1) and\n\t// tells a reader how much history there is.\n\tif truncated {\n\t\tb.WriteString(\"\\n_Showing the \" + strconv.Itoa(len(rows)) +\n\t\t\t\" most recent acts. This court has moderated \" + strconv.Itoa(cm.claims.Size()) +\n\t\t\t\" claims; older acts are not shown._\\n\")\n\t}\n\treturn b.String()\n}\n\n// appealVerbGloss says in words what a review verb does.\n//\n// The page showed the bare code — \"action: **setmods**\" — on the surface where\n// the review court's heaviest acts are approved: seating moderators on somebody\n// else's court, disarming a set, hiding a claim platform-wide. A voter asked to\n// approve an act named only by its code is being asked to approve a word.\n//\n// The wording comes from the constants' own comments in meta.gno, so there is\n// one description of each verb and not two that can disagree.\nfunc appealVerbGloss(verb string) string {\n\tswitch verb {\n\tcase verbUnhide:\n\t\treturn \"un-hides a claim its own court's moderators hid\"\n\tcase verbClear:\n\t\treturn \"clears a hide the review court itself placed\"\n\tcase verbHide:\n\t\treturn \"hides a claim across the platform, over its court's head\"\n\tcase verbSuspend:\n\t\treturn \"disarms a court's moderator set — they can take no further act\"\n\tcase verbUnsuspend:\n\t\treturn \"re-arms a suspended moderator set, and re-applies the hides that \" +\n\t\t\t\"were masked while it was disarmed\"\n\tcase verbSetmods:\n\t\treturn \"installs a moderator set on that court, replacing whoever holds \" +\n\t\t\t\"the seats now\"\n\t}\n\t// Not reachable through AppealParse, which validates the verb — but a render\n\t// that invents a description for an unknown verb would be worse than one\n\t// that admits it does not know.\n\treturn \"an action this page does not recognise\"\n}\n"},{"name":"modvote.gno","body":"// The moderator-replacement election (MODERATION.md §3.1) — textbook approval\n// voting, specified carefully.\n//\n// This mechanism broke in FOUR consecutive audit rounds (latch-camp →\n// per-proposer-sybil → bond-auction ballot-veto → majority-gate + candidate-\n// cap). Every break had the same shape: a SCARCE EXCLUDABLE SLOT that capital\n// could win in order to keep a proposal off the vote. The converged design has\n// none:\n//\n//   - the ballot is UNCAPPED — bonds ADD candidates and can never exclude one;\n//   - \"retain the current set\" is an EXPLICIT APPROVABLE LINE, not a silent\n//     zero-approval default (an absolute bps threshold is meaningless in\n//     approval voting, which has no yes+no denominator, and it made abstention\n//     a vote for the incumbent: a 31% captured minority beat a 69% majority);\n//   - install is argmax-approval OVER RETAIN plus a margin and a quorum;\n//   - the bond refunds ONLY on install, so self-approving junk always costs;\n//   - β is frozen below the quorum floor AND the bond is clamped to that floor\n//     at runtime, so affording the bond is never tighter than winning the vote.\n//\n// Resolution is deliberately TWO-PASS: pass 1 finds the max and retain, pass 2\n// picks the earliest-nominated candidate within the margin of the max. A\n// single-pass running argmax gets the tie-break wrong and would let a whale\n// install a late one-address-swapped DECOY of an honest set.\n\npackage kourtv2\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\n\tgrc20votes \"gno.land/p/kourt/grc20votes/v0\"\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\nconst (\n\t// nominationWindow is load-bearing (round-7 R4): the per-court latch makes\n\t// it a temporal-griefing surface, so it must be long enough that a defender\n\t// can always nominate INTO a griefer's open election rather than having to\n\t// win a race. One day.\n\tnominationWindow = int64(17_280)\n\n\t// maxBallotLines bounds the work ResolveElection must do in ONE transaction.\n\t// Without it the failure mode is permanent, not merely slow: the bond is\n\t// pinned at OpenElection while the curve keeps selling for the whole\n\t// nomination window, so an attacker can fund far more lines than the bond\n\t// implied, and a resolve that cannot fit in a tx leaves `resolved == false`\n\t// — which refuses every future election on that court forever AND strands\n\t// every poster's bond. The cap is deliberately far above honest use, and it\n\t// is NOT the r6 \"excludable slot\": a shut-out nominator opens the next\n\t// election themselves rather than being kept off a vote that proceeds.\n\tmaxBallotLines = 64\n\n\t// electionBondBps is β: the per-candidate bond as a fraction of supply,\n\t// floored at carrotClampMinCC. Frozen — never an owner knob. Its ceiling is\n\t// checked against the quorum floor at deploy (mustElectionInvariants).\n\telectionBondBps = int64(50) // 0.5% of supply, vs the 5% quorum floor\n)\n\n// mustElectionInvariants is the two-sided deploy gate on β (round-7 R1): a\n// floor keeping the bond meaningful, and a ceiling keeping it strictly below\n// the quorum floor — so any coalition that can clear quorum can trivially\n// afford to field one nominator, and no court is ever challenge-proof by price.\n//\n// A constant gate can only police the RATIO arm, and only if both sides are\n// measured on the same base. Two runtime terms escape it entirely — the\n// absolute carrotClampMinCC minimum, and (before this fix) a bond quoted on raw\n// supply against a floor quoted on votable weight. electionBond now closes\n// both at runtime; see its comment. This gate is the ratio arm alone.\n//\n// ITS THREE PANICS ARE IN NO MUTATION BATCH, AND THAT IS DELIBERATE. Each\n// compares package CONSTANTS, and init() calls this function on every package\n// load — including every test load — so the relationship is already enforced on\n// every run by the guard itself: violate it and the package does not load.\n// Deleting a panic here is unobservable precisely BECAUSE the constants satisfy\n// it, so the mutation survives correctly rather than revealing a gap, and a test\n// restating the same comparison would duplicate init() instead of adding a\n// check. It is not in known-gaps either: that file asserts a gap a test could\n// someday close, and this one never can — v0.40's rule, the same one that keeps\n// ResetModSet's clear out of both batches. If you change electionBondBps,\n// quorumSupplyBps or nominationWindow, THIS is what stops you.\nfunc mustElectionInvariants() {\n\tif electionBondBps \u003c= 0 {\n\t\tpanic(\"kourtv2: the election bond must be positive\")\n\t}\n\tif electionBondBps \u003e= quorumSupplyBps {\n\t\tpanic(\"kourtv2: the election bond must stay below the quorum floor — else affording the ballot is harder than winning the vote (incumbency lock by price)\")\n\t}\n\tif nominationWindow \u003c 17_280 {\n\t\tpanic(\"kourtv2: the nomination window must be at least a day\")\n\t}\n}\n\nfunc init() { mustElectionInvariants() }\n\n// modCandidate is a pre-registered candidate moderator set. Registration is\n// separate from nomination so the appeal/ballot carries a short ID rather than\n// an address list, and so the SAME primitive serves the meta court's\n// `mod:setmods` verb (the peer install path).\ntype modCandidate struct {\n\tid      uint64\n\tmembers []address // canonical: sorted and deduped\n\tm       int\n\tby      address\n\tat      int64\n}\n\n// ballotLine is one candidate's line on an open ballot.\ntype ballotLine struct {\n\tcandID uint64\n\tweight int64\n\tnomAt  int64\n\tbond   int64\n\tposter address\n}\n\n// election is one court's open ballot.\ntype election struct {\n\tseq         uint64\n\tepoch       uint32 // weights pinned at nomination-window open\n\topenedAt    int64\n\tnominateEnd int64\n\tvoteEnd     int64\n\t// The two deadlines again as unix seconds. A STORED FUTURE HEIGHT CANNOT BE\n\t// REPAIRED LATER: it is already wrong the moment cadence changes after it was\n\t// written, and the `now` it was derived from is gone, so unlike an event stamp\n\t// there is nothing a fallback could reconstruct. They are therefore written\n\t// together with their heights and never derived afterwards. Zero on ballots\n\t// opened before the stamps, where the heights are still the rule.\n\tnominateEndTime int64\n\tvoteEndTime     int64\n\tfloor       int64 // quorum floor AND margin floor (escrow-netted 5%)\n\tbond        int64 // per-candidate bond, fixed at open\n\tlines       *bptree.BPTree\n\tretainW     int64\n\tnominators  *bptree.BPTree // addr -\u003e true (one nomination per address)\n\tvoters      *bptree.BPTree // addr -\u003e the weight it voted at (see approve)\n\tapprovals   *bptree.BPTree // be64(candID)|addr -\u003e true (per-line dedup)\n\tturnout     int64\n\tresolved    bool\n}\n\n// electionFloor is the quorum AND margin floor: 5% of VOTABLE weight, netting\n// out the escrow. Netting matters — on an active court most CC sits in escrow\n// as stake/bonds, and a floor of 5% of RAW supply can exceed all votable weight,\n// making the electorate's own remedy unreachable exactly where it is needed.\nfunc electionFloor(c *Court, at uint32) int64 {\n\tf := mulDiv128(votableAt(c, at), quorumSupplyBps, grc20votes.Bps)\n\tif f \u003c 1 {\n\t\tf = 1\n\t}\n\treturn f\n}\n\n// votableAt is supply net of the escrow — the base BOTH the floor and the bond\n// must be measured on. Quoting one on raw supply and the other on votable is\n// not a rounding difference: it multiplies the bond/floor ratio by\n// supply/votable, which on a busy court is unbounded.\nfunc votableAt(c *Court, at uint32) int64 {\n\tv := c.coin.PastTotal(at) - c.coin.PastVotes(c.escrow, at)\n\tif v \u003c 0 {\n\t\tv = 0\n\t}\n\treturn v\n}\n\n// electionBond is β of VOTABLE weight, then clamped to never exceed the\n// election floor. Both clamps repair a break in the deploy invariant that\n// mustElectionInvariants structurally cannot see:\n//\n//   - BASE MISMATCH. The bond used to be quoted on raw supply while the floor\n//     nets the escrow, making the true ratio (β/q)·(supply/votable). β/q is\n//     1/10, so the invariant inverted on any court holding more than 90% of its\n//     CC as stake and bonds — i.e. exactly the mature, active courts whose\n//     electorate most needs the remedy, and only those. Same base now.\n//   - ABSOLUTE FLOOR. carrotClampMinCC (1 CC) is a constant, so on a court with less\n//     than 20 CC votable it is the binding term and exceeds 5% of votable\n//     outright. The clamp lets the floor win: a spam bar priced above winning\n//     the vote is the incumbency-lock-by-price the invariant forbids, and the\n//     bar is not needed there anyway — filling all 64 ballot lines at 5% of\n//     votable each costs 3.2× the court's entire votable weight.\nfunc electionBond(c *Court, at uint32) int64 {\n\tb := mulDiv128(votableAt(c, at), electionBondBps, grc20votes.Bps)\n\tif b \u003c carrotClampMinCC {\n\t\tb = carrotClampMinCC\n\t}\n\tif f := electionFloor(c, at); b \u003e f {\n\t\tb = f\n\t}\n\treturn b\n}\n\n// RegisterModCandidate stores a candidate moderator set and returns its id.\n// Permissionless (it is only a named address list; nominating it costs the\n// bond). Members are canonicalized — sorted and deduped — so a mere reordering\n// is not a distinct candidate, which is half of the anti-decoy rule.\nfunc RegisterModCandidate(cur realm, courtSlug string, m int, members ...address) uint64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\tcanon := canonicalMembers(members)\n\tif len(canon) == 0 {\n\t\tpanic(\"kourtv2: a candidate set may not be empty\")\n\t}\n\tif m \u003c 1 || m \u003e len(canon) {\n\t\tpanic(\"kourtv2: the m-of-n threshold must be in 1..n\")\n\t}\n\tif cm.candidates == nil {\n\t\tcm.candidates = bptree.NewBPTree32()\n\t}\n\tcm.candSeq++\n\tid := cm.candSeq\n\tcm.candidates.Set(beClaimKey(id), \u0026modCandidate{\n\t\tid: id, members: canon, m: m, by: who, at: heightNow(),\n\t})\n\treturn id\n}\n\n// canonicalMembers sorts and dedupes an address list (insertion sort — a\n// moderator set is small, and gno needs determinism, not speed).\nfunc canonicalMembers(in []address) []address {\n\t// The size cap is enforced HERE because this is the one chokepoint both\n\t// entry points share (AppointMods and RegisterModCandidate). Uncapped, a\n\t// set could be grown until currentSetID — which concatenates every member\n\t// into one string and therefore costs O(n²) — no longer fits in a\n\t// transaction. currentSetID sits on the SUSPEND path, so that is not merely\n\t// slow: it would blunt the global DAO's and the meta court's emergency\n\t// hammer against exactly the set that made itself too big to suspend.\n\tif len(in) \u003e maxModSetSize {\n\t\tpanic(\"kourtv2: a moderator set is at most 32 members\")\n\t}\n\tout := []address{}\n\tfor _, a := range in {\n\t\tif a == \"\" {\n\t\t\tpanic(\"kourtv2: a moderator address may not be zero\")\n\t\t}\n\t\t// The zero check above is kept SEPARATE and FIRST because it names the\n\t\t// commonest mistake precisely. This one catches everything else: the\n\t\t// argument conversion that turns a transaction's strings into VM values\n\t\t// does NOT validate bech32 — its string arm simply sets the value — so a\n\t\t// mistyped address arrives here intact, gets canonicalised, and is SEATED\n\t\t// as a moderator. The set then carries a member nobody holds the key to,\n\t\t// and its m-of-n threshold counts that member: an m-of-2 set with one\n\t\t// typo can never reach its own threshold again, which is the same\n\t\t// permanent-freeze shape the {A,A,B} m=3 case produces one guard below.\n\t\t// Both entry points funnel through here, so this is the only place it has\n\t\t// to be said.\n\t\tif !a.IsValid() {\n\t\t\tpanic(\"kourtv2: not a valid address for a moderator set\")\n\t\t}\n\t\tdup := false\n\t\tfor _, b := range out {\n\t\t\tif b == a {\n\t\t\t\tdup = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif dup {\n\t\t\tcontinue\n\t\t}\n\t\tpos := len(out)\n\t\tout = append(out, a)\n\t\tfor pos \u003e 0 \u0026\u0026 out[pos-1].String() \u003e a.String() {\n\t\t\tout[pos] = out[pos-1]\n\t\t\tpos--\n\t\t}\n\t\tout[pos] = a\n\t}\n\treturn out\n}\n\nfunc (cm *courtMod) mustCandidate(id uint64) *modCandidate {\n\tif cm.candidates == nil {\n\t\tpanic(\"kourtv2: no such candidate\")\n\t}\n\tv := cm.candidates.Get(beClaimKey(id))\n\tif v == nil {\n\t\tpanic(\"kourtv2: no such candidate\")\n\t}\n\treturn v.(*modCandidate)\n}\n\n// electionCooldownOpen reports whether a court is still inside the cooldown a\n// fully-failed election starts.\n//\n// A THIRD STORED FUTURE MOMENT, converted like the other two: the twin is\n// written beside the height and never derived afterwards. The duration is\n// decideWindowBlocks — \"the realm's unit for a body gets this long to decide\",\n// reused here — so it converts at the write site rather than through a constant\n// of its own, for the reason pendingTTLBlocks' note gives.\n//\n// A ZERO COOLDOWN IS \"NEVER SET\", and both arms answer that correctly without a\n// special case: with no stamp the height arm asks `heightNow() \u003c 0`, which is\n// false, and a court that has never failed an election is never in cooldown.\nfunc electionCooldownOpen(cm *courtMod) bool {\n\tpassed, known := pastDeadline(cm.electionCooldownUntilTime, 0)\n\treturn (known \u0026\u0026 !passed) || (!known \u0026\u0026 heightNow() \u003c cm.electionCooldownUntil)\n}\n\n// nominationClosed and votingClosed are the ballot's PHASE, asked in one place\n// each. Six sites ask them — three gates in modvote, one in ResolveElection and\n// the two the render derives its phase banner from — and the phases are adjacent,\n// which is why they convert together and share these two functions.\n//\n// A HALF-CONVERTED PHASE MACHINE HAS AN INCOHERENT MIDDLE. `approve` tests both\n// boundaries back to back: if nomination closed on the clock while voting closed\n// on blocks, a drifting chain gets instants where neither window is open, or\n// where a ballot accepts votes the render calls closed. That is not a smaller\n// version of the same bug, it is a different one, so nominateEnd and voteEnd\n// move in one commit.\n//\n// secs is 0 because these stamps ARE the deadline, not an event to add a window\n// to — the same shape escrowUntilAt uses in dispute.gno.\nfunc nominationClosed(e *election) bool {\n\tpassed, known := pastDeadline(e.nominateEndTime, 0)\n\treturn (known \u0026\u0026 passed) || (!known \u0026\u0026 heightNow() \u003e= e.nominateEnd)\n}\n\nfunc votingClosed(e *election) bool {\n\tpassed, known := pastDeadline(e.voteEndTime, 0)\n\treturn (known \u0026\u0026 passed) || (!known \u0026\u0026 heightNow() \u003e= e.voteEnd)\n}\n\n// OpenElection opens the ballot AND nominates the opener's candidate in one\n// call. Opening is deliberately NOT free: a free open would let a griefer burn\n// a court's governance for ~15 days a cycle (1d nomination + 7d vote + 7d\n// cooldown) at zero cost. Every election therefore starts with one bonded\n// candidate.\nfunc OpenElection(cur realm, courtSlug string, candidateID uint64) uint64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\tif cm.election != nil \u0026\u0026 !cm.election.resolved {\n\t\tpanic(\"kourtv2: an election is already open for this court\")\n\t}\n\tnow := heightNow()\n\tif electionCooldownOpen(cm) {\n\t\tpanic(\"kourtv2: this court is in its post-election cooldown\")\n\t}\n\tcand := cm.mustCandidate(candidateID)\n\tat := c.coin.Epoch() - 1\n\tcm.electionSeq++\n\te := \u0026election{\n\t\tseq:         cm.electionSeq,\n\t\tepoch:       at,\n\t\topenedAt:    now,\n\t\tnominateEnd: now + nominationWindow,\n\t\tvoteEnd:     now + nominationWindow + c.params.votingBlocks,\n\n\t\tnominateEndTime: nowTime() + blocksToSecs(nominationWindow),\n\t\tvoteEndTime:     nowTime() + blocksToSecs(nominationWindow+c.params.votingBlocks),\n\t\tfloor:       electionFloor(c, at),\n\t\tbond:        electionBond(c, at),\n\t\tlines:       bptree.NewBPTree32(),\n\t\tnominators:  bptree.NewBPTree32(),\n\t\tvoters:      bptree.NewBPTree32(),\n\t\tapprovals:   bptree.NewBPTree32(),\n\t}\n\tcm.election = e\n\taddNomination(c, cm, e, cand, who)\n\treturn e.seq\n}\n\n// NominateCandidate adds a candidate to the OPEN ballot. There is no cap: a\n// bond ADDS a line and can never keep one off. One nomination per address, so\n// a single key cannot spam the page; sybil keys can, but each pays the bond.\nfunc NominateCandidate(cur realm, courtSlug string, candidateID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\te := cm.mustOpenElection()\n\tif nominationClosed(e) {\n\t\tpanic(\"kourtv2: the nomination window has closed\")\n\t}\n\tcand := cm.mustCandidate(candidateID)\n\taddNomination(c, cm, e, cand, who)\n}\n\nfunc addNomination(c *Court, cm *courtMod, e *election, cand *modCandidate, who address) {\n\tif e.lines.Size() \u003e= maxBallotLines {\n\t\tpanic(\"kourtv2: this ballot is full; open the next election instead\")\n\t}\n\tif e.nominators.Has(who.String()) {\n\t\tpanic(\"kourtv2: one nomination per address per election\")\n\t}\n\tif e.lines.Has(beClaimKey(cand.id)) {\n\t\tpanic(\"kourtv2: that candidate is already on the ballot\")\n\t}\n\tmustSpendable(c, who, e.bond)\n\tc.coin.Transfer(who, c.escrow, e.bond) // the poster's own CC (the I2 carve-out)\n\te.nominators.Set(who.String(), true)\n\te.lines.Set(beClaimKey(cand.id), \u0026ballotLine{\n\t\tcandID: cand.id, nomAt: heightNow(), bond: e.bond, poster: who,\n\t})\n}\n\nfunc (cm *courtMod) mustOpenElection() *election {\n\tif cm.election == nil || cm.election.resolved {\n\t\tpanic(\"kourtv2: no election is open\")\n\t}\n\treturn cm.election\n}\n\n// ApproveCandidate approves one candidate line. A voter may approve ANY number\n// of lines including retain — that is what makes adding an honest candidate\n// unable to split a majority.\nfunc ApproveCandidate(cur realm, courtSlug string, candidateID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tapprove(cur.Previous().Address(), courtSlug, candidateID, false)\n}\n\n// ApproveRetain approves keeping the current moderator set. Retain is a real\n// ballot line judged by approvals, never a silent default.\nfunc ApproveRetain(cur realm, courtSlug string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tapprove(cur.Previous().Address(), courtSlug, 0, true)\n}\n\nfunc approve(who address, courtSlug string, candidateID uint64, retain bool) {\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\te := cm.mustOpenElection()\n\tif !nominationClosed(e) {\n\t\tpanic(\"kourtv2: voting opens when the nomination window closes\")\n\t}\n\tif votingClosed(e) {\n\t\tpanic(\"kourtv2: voting has closed\")\n\t}\n\t// Weighed at the epoch OpenElection pinned, floored by what the voter still\n\t// holds, and computed ONCE PER ELECTION — at the voter's first approval, then\n\t// reused for every later line on the same ballot.\n\t//\n\t// THE REUSE IS NOT AN OPTIMISATION, it is what keeps the tally arithmetic.\n\t// The floor is a LIVE read and a ballot spans many transactions, so a\n\t// re-derived figure can move between two of one voter's approvals: approve\n\t// line A holding little, top up, approve line B at the full snapshot, and\n\t// line B now carries weight ABOVE the turnout recorded back at approval A.\n\t// install compares maxW against e.turnout and e.floor, so a line heavier than\n\t// the turnout meant to bound it is a candidate installing on weight the\n\t// election never counted. Freezing at first approval makes max(line.weight) \u003c=\n\t// turnout arithmetic instead of a hope.\n\t//\n\t// THE RATIO IS UNBOUNDED, and an earlier version of this comment borrowed \"10x\n\t// the whole turnout\" from VOTELOCK.md — a figure the panels measured on the\n\t// REVERTED design, where both readings were live. That is a different mechanism\n\t// and the number does not transfer. Here the ceiling still caps at the snapshot,\n\t// so with the freeze removed a voter who approves line A holding e and then tops\n\t// up gets line B at the snapshot against a turnout of e: the ratio is\n\t// snapshot/e, arbitrarily large as e falls to one unit. Derivable rather than\n\t// measured, and strictly worse than the borrowed figure — which is why carrying\n\t// a magnitude across a design boundary is worth catching even when the truth\n\t// turns out to be the less flattering number.\n\t//\n\t// A concrete instance, by arithmetic on the shipped fixture's own constants\n\t// rather than by a fresh measurement: TestElectionWeightIsFrozenAtFirstApproval\n\t// gives its voter a 100 B snapshot and has them approve while retaining about\n\t// 1 B, so ablating the freeze there puts line B at roughly 100x the recorded\n\t// turnout — an order of magnitude past the 10x that was borrowed.\n\t//\n\t// It is also why e.voters holds the WEIGHT rather than a presence bool. \"This\n\t// address has voted\" and \"at what weight\" have to be one fact in one place, or\n\t// they can disagree.\n\t//\n\t// This was the lane most exposed to rented weight — the opener chooses the\n\t// anchor and a coup is court-wide rather than one claim, measured at 12x this\n\t// floor from a one-epoch rental on a 500 B court. The floor is what closes it:\n\t// the renter must return the coin to repay, and then there is nothing to vote.\n\tvar w int64\n\tif prior := e.voters.Get(who.String()); prior != nil {\n\t\tw = prior.(int64)\n\t} else {\n\t\tvar snapshot int64\n\t\tw, snapshot = votingWeight(c, who, e.epoch)\n\t\tif snapshot \u003c= 0 {\n\t\t\tpanic(\"kourtv2: no voting weight at the pinned epoch\")\n\t\t}\n\t\tif w \u003c= 0 {\n\t\t\tpanic(\"kourtv2: you no longer hold the coin you would vote with\")\n\t\t}\n\t}\n\tkey := beClaimKey(candidateID) + who.String()\n\tif retain {\n\t\tkey = \"retain\" + who.String()\n\t}\n\tif e.approvals.Has(key) {\n\t\tpanic(\"kourtv2: already approved that line\")\n\t}\n\tif !retain {\n\t\tv := e.lines.Get(beClaimKey(candidateID))\n\t\tif v == nil {\n\t\t\tpanic(\"kourtv2: that candidate is not on the ballot\")\n\t\t}\n\t\tline := v.(*ballotLine)\n\t\tline.weight += w\n\t} else {\n\t\te.retainW += w\n\t}\n\te.approvals.Set(key, true)\n\t// Turnout is DISTINCT-VOTER weight, not the sum of approvals — a voter who\n\t// approves five lines is one voter, not five (round-7 R2).\n\tif !e.voters.Has(who.String()) {\n\t\te.voters.Set(who.String(), w)\n\t\te.turnout += w\n\t\t// Committed until the election resolves. Taken HERE, on the first approval,\n\t\t// for the same reason the weight is frozen here: one voter is one\n\t\t// commitment, and approving five lines must not lock five times the coin.\n\t\t// See votelock.gno.\n\t\tlockVote(c, who, voteLockElection, int64(e.seq), 0, w)\n\t}\n}\n\n// ResolveElection closes the ballot and installs the winner, or retains.\n// Permissionless once the voting window has passed.\nfunc ResolveElection(cur realm, courtSlug string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\te := cm.mustOpenElection()\n\tnow := heightNow()\n\tif !votingClosed(e) {\n\t\tpanic(\"kourtv2: the voting window has not closed\")\n\t}\n\te.resolved = true\n\n\t// PASS 1 — the maximum candidate approval (retain is read directly).\n\tmaxW := int64(0)\n\te.lines.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tif l, ok := v.(*ballotLine); ok \u0026\u0026 l.weight \u003e maxW {\n\t\t\tmaxW = l.weight\n\t\t}\n\t\treturn false\n\t})\n\n\t// The install condition: a challenger must WIN and beat retain by at least\n\t// the margin, on at least quorum turnout. Otherwise the current set stands\n\t// (fail-safe on indecision — the right direction for governance).\n\tinstall := maxW \u003e 0 \u0026\u0026 e.turnout \u003e= e.floor \u0026\u0026 maxW-e.retainW \u003e= e.floor\n\n\t// PASS 2 — among candidates within the margin of the max, the EARLIEST\n\t// NOMINATED wins. A decoy clone of an honest set is necessarily nominated\n\t// later, so it can never displace the original inside the margin.\n\tvar winner *ballotLine\n\tif install {\n\t\te.lines.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\t\tl, ok := v.(*ballotLine)\n\t\t\t// Both tests matter. The first keeps the tie-break inside the margin\n\t\t\t// band; the SECOND re-applies the install condition to the line that\n\t\t\t// actually wins. Without it the band is `weight \u003e= maxW - floor`, so\n\t\t\t// when maxW is near the floor the band reaches down to ZERO and the\n\t\t\t// earliest-nominated line installs on no approvals at all — and the\n\t\t\t// election opener is always the earliest line, so a griefer's puppet\n\t\t\t// set would beat a genuinely-approved challenger for free.\n\t\t\tif !ok || l.weight \u003c maxW-e.floor || l.weight-e.retainW \u003c e.floor {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif winner == nil || l.nomAt \u003c winner.nomAt ||\n\t\t\t\t(l.nomAt == winner.nomAt \u0026\u0026 l.candID \u003c winner.candID) {\n\t\t\t\twinner = l\n\t\t\t}\n\t\t\treturn false\n\t\t})\n\t}\n\n\t// Bond dispositions: refunded ONLY to an installed candidate's poster;\n\t// every other line half-burns. Refunding an \"above quorum\" loser would let\n\t// a ≥5%-holder self-approve junk to reach the floor and spam the ballot for\n\t// free (round-6 F3).\n\te.lines.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tl, ok := v.(*ballotLine)\n\t\tif !ok || l.bond \u003c= 0 {\n\t\t\treturn false\n\t\t}\n\t\tif winner != nil \u0026\u0026 l.candID == winner.candID {\n\t\t\tc.coin.Transfer(c.escrow, l.poster, l.bond)\n\t\t} else {\n\t\t\thalf := l.bond / 2\n\t\t\tif half \u003e 0 {\n\t\t\t\tc.coin.Transfer(c.escrow, l.poster, half)\n\t\t\t}\n\t\t\tif burn := l.bond - half; burn \u003e 0 {\n\t\t\t\tc.coin.Burn(c.escrow, burn)\n\t\t\t}\n\t\t}\n\t\tl.bond = 0\n\t\treturn false\n\t})\n\n\tif winner != nil {\n\t\tcand := cm.mustCandidate(winner.candID)\n\t\tinstallModSet(c, cm, cand, now, true)\n\t\tchain.Emit(\"ModSetInstalled\",\n\t\t\t\"court\", c.id, \"by\", \"election\",\n\t\t\t\"candidate\", strconv.FormatUint(cand.id, 10),\n\t\t\t\"height\", strconv.FormatInt(now, 10),\n\t\t)\n\t\treturn\n\t}\n\t// A fully-failed election starts this court's OWN cooldown slot (the\n\t// decideWindowBlocks DURATION is reused; the per-claim flag field is not).\n\tcm.electionCooldownUntil = now + decideWindowBlocks\n\tcm.electionCooldownUntilTime = nowTime() + blocksToSecs(decideWindowBlocks)\n}\n\n// installModSet is the shared primitive both peers use to replace a court's\n// moderator set, so last-writer-wins is well defined. It always replaces the\n// members/threshold, stamps setActHeight, and CLEARS the suspension — a freshly\n// installed set starts ARMED, because a suspension is a judgment about the set\n// that was judged, not a property of the court; carrying it onto an unjudged\n// set would punish a party no authority evaluated and would make the verb inert\n// in the very case that motivates it (negligent mods).\n//\n// Two effects are ELECTION-ONLY (the setmods vet's consensus). A meta install\n// must not:\n//   - stamp lastElectionAt, or one cheap meta appeal would poison every other\n//     in-flight appeal on that court (including restorative unhides, whose\n//     staleness reads exactly that field — M-A40 through the back door); nor\n//   - burn creatorUnseated, or one cheap meta appeal would permanently destroy\n//     the free, instant AppointMods repair path of a court that has never held\n//     an election.\n//\n// The local electorate's own verdict is a different thing: it legitimately ends\n// the creator's appointment power and legitimately stamps the election clock.\nfunc installModSet(c *Court, cm *courtMod, cand *modCandidate, at int64, byElection bool) {\n\tfresh := bptree.NewBPTree32()\n\tfor _, a := range cand.members {\n\t\tfresh.Set(a.String(), true)\n\t}\n\tcm.members = fresh\n\tcm.n = len(cand.members)\n\tcm.m = cand.m\n\t// THE hole (found independently by three reviewers). This is the shared\n\t// install primitive for BOTH peers that replace a set — the court's own\n\t// election and the meta court's `mod:setmods` verdict — and it was the only\n\t// membership change that left the old set's approvals standing. Old set at\n\t// m-1 on a hide, electorate throws them out, one new member fires it on the\n\t// deposed signatures: exactly the \"a lone puppet installed into a set can\n\t// act on nothing\" bound (§3.1), broken in the scenario the election IS the\n\t// remedy for, and reachable through permissionless ResolveElection.\n\tclearPendingOnMembershipChange(\u0026cm.pending)\n\tcm.setActHeight = at\n\tcm.installedByMeta = !byElection\n\t// Clear the suspension UNLESS this install re-seats the very set that was\n\t// judged: always-clearing would make suspension escapable by re-installing\n\t// the same addresses, while never-clearing would deliver a pre-disarmed\n\t// remedy. A one-address swap still escapes — that is the accepted decoy\n\t// residual, and it costs a real membership change.\n\t//\n\t// THE THRESHOLD IS NOT A MEMBERSHIP CHANGE (v0.52, 3-0 consensus). The\n\t// identity used to be `m + \":\" + members` compared whole, so a 2-of-2 {A,B}\n\t// could re-install itself as 1-of-2 {A,B} and walk out of its own\n\t// suspension: the same people, not one seat surrendered, and strictly EASIER\n\t// to act than the body that was judged. The election route could not even\n\t// see it — a ballot exposes members and not m — so the membership diff\n\t// voters are asked to perform was blind to a threshold-only clone.\n\t//\n\t// So membership is compared on its own, and an unchanged membership lifts a\n\t// suspension only by TIGHTENING. Raising m is de-escalatory: it hands the\n\t// judged actors strictly less unilateral power, which is a real remedy and\n\t// worth rewarding. Holding or lowering it is not. Nothing is stranded\n\t// either way — `mod:unsuspend` and the global DAO's ClearCourtSuspension\n\t// remain explicit re-arm paths that answer to their own authority.\n\tif currentSetID(cm) != cm.suspendedSetID || cm.m \u003e cm.suspendedM {\n\t\tcm.suspended = false\n\t\tcm.suspendedSetID = \"\"\n\t\tcm.suspendedM = 0\n\t}\n\tif byElection {\n\t\tcm.lastElectionAt = at\n\t\tcm.creatorUnseated = true\n\t}\n}\n\n// mustModRead is the READ-ONLY counterpart of ensureMod: a query must never\n// allocate and persist a courtMod as a side effect of being asked a question.\nfunc mustModRead(courtSlug string) *courtMod {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\tpanic(\"kourtv2: this court has no moderation state yet\")\n\t}\n\treturn c.mod\n}\n\n// ---- reads ----------------------------------------------------------------\n\n// ElectionOpen reports whether a court has a live ballot.\nfunc ElectionOpen(courtSlug string) bool {\n\tc := mustCourt(courtSlug)\n\treturn c.mod != nil \u0026\u0026 c.mod.election != nil \u0026\u0026 !c.mod.election.resolved\n}\n\n// ElectionWindows returns (nominateEnd, voteEnd) of the open ballot.\nfunc ElectionWindows(courtSlug string) (int64, int64) {\n\te := mustModRead(courtSlug).mustOpenElection()\n\treturn e.nominateEnd, e.voteEnd\n}\n\n// ElectionFloorOf is the weight bar this court's open election must clear,\n// pinned when the election opened and frozen since: 5% of the votable supply at\n// the epoch the opening chose.\n//\n// IT IS TWO BARS AT ONCE. Total turnout must reach it, AND the leading\n// candidate must beat \"retain\" by at least this much. Miss either and NOTHING\n// INSTALLS — the current moderator set simply stands. So a coalition that\n// mobilises to what it thinks the bar is and falls short hands the election to\n// the incumbents.\n//\n// Mobilise against CASTABLE weight, not against the pool. The base is `votable`,\n// which nets out the court escrow and nothing else: coin at a wrapper's address\n// is inside it and no key can vote it (see VotableSupply). ccwrap caps its vault\n// at 35% of votable, so the shortfall is bounded there while that cap holds.\n//\n// Refused when the court does not exist, has no moderation state yet, or has no\n// open election.\nfunc ElectionFloorOf(courtSlug string) int64 {\n\treturn mustModRead(courtSlug).mustOpenElection().floor\n}\n\n// ElectionBondOf is the per-candidate bond pinned at open.\nfunc ElectionBondOf(courtSlug string) int64 {\n\treturn mustModRead(courtSlug).mustOpenElection().bond\n}\n\n// ElectionTally exposes a line's approving weight and retain's, for render.\nfunc ElectionTally(courtSlug string, candidateID uint64) (candW, retainW, turnout int64) {\n\te := mustModRead(courtSlug).mustOpenElection()\n\tif v := e.lines.Get(beClaimKey(candidateID)); v != nil {\n\t\tcandW = v.(*ballotLine).weight\n\t}\n\treturn candW, e.retainW, e.turnout\n}\n\n// CandidateMembers returns a copy of a candidate's canonical address list, so\n// render can DIFF it against the incumbent (the other half of the anti-decoy\n// rule: a one-address swap must be visible, not just flagged).\nfunc CandidateMembers(courtSlug string, candidateID uint64) []address {\n\tcand := mustModRead(courtSlug).mustCandidate(candidateID)\n\tout := make([]address, len(cand.members))\n\tcopy(out, cand.members)\n\treturn out\n}\n\n// CandidateThreshold reports how many of a candidate set's members would have to\n// agree. Voters need it standing next to CandidateMembers: the addresses alone\n// do not say how many must sign, so a candidate that keeps every incumbent and\n// only LOOSENS the rule is otherwise invisible on the ballot — and that is\n// precisely the change the suspension rule refuses to treat as a remedy. The\n// meta court's appeal page has shown this since the setmods vet (writeAppeal);\n// the election ballot had no equivalent.\nfunc CandidateThreshold(courtSlug string, candidateID uint64) int {\n\treturn mustModRead(courtSlug).mustCandidate(candidateID).m\n}\n\n// ElectionCooldownUntil is the height a failed election blocks a new one until.\nfunc ElectionCooldownUntil(courtSlug string) int64 {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil {\n\t\treturn 0\n\t}\n\treturn c.mod.electionCooldownUntil\n}\n"},{"name":"openrewards.gno","body":"// OPEN THE REWARDS (PLAN §10.2 Q6-Q8): the one moment a claim's emission becomes\n// concrete. Everything before it is principal (never gated) or reservation;\n// everything after is pull-claims. The draw is WALK-FREE: the denominators are\n// the side-level pool integrals (exact by construction — advancePools), so no\n// step here iterates stakers, and every individual payment applies P10's\n// scale-then-cap at ITS OWN pull. Cap-cut and rounding dust stays reserved\n// and unminted — economically a burn, always on the conservative side of the\n// 20% ceiling.\n\npackage kourtv2\n\nimport (\n\t\"math/bits\"\n)\n\n// OpenRewards computes a settled claim's draw D and opens the pull-claims.\n// Gates (Q6): verdict final; every quality question closed and 24h quiet\n// (flags pause THIS, never principal); participant-only for the first week\n// of the verdict (A13 — a stranger must not pick the reservoir moment).\nfunc OpenRewards(cur realm, courtSlug string, claimID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif cs.rewardsOpened {\n\t\tpanic(\"kourtv2: already rewardsOpened\")\n\t}\n\tif cs.verdictAt == 0 {\n\t\tpanic(\"kourtv2: no final verdict yet\")\n\t}\n\t// S3: provClose now DRAWS. Only a dead claim has no draw — it has no answer, no\n\t// provisional and therefore no winning side to pay.\n\tif cs.closed {\n\t\tpanic(\"kourtv2: dead claims have no draw\")\n\t}\n\t// THE 24h QUIET WINDOW STOOD HERE and went with the quality lane. It held the\n\t// draw for a day after the last FLAG event so a resolved flag could not be\n\t// rewardsOpened out from under a replacement flag. lastFlagEventAt was written\n\t// only by the flag lane, so with the lane gone nothing stamps it, the field is\n\t// zero on every claim, and the guard would compare against block 0 — a check\n\t// that can only fire in the first day of a chain's life. Deleted rather than\n\t// left reading a field nobody writes.\n\tnow := heightNow()\n\t/* THE WEEK IS A WEEK, NOT 120,960 BLOCKS. Counted in blocks this gate held\n\t   long past its own week whenever the chain ran slower than 5s — measured on\n\t   kourt-1, a claim settled in April 2022 was still inside its grace in\n\t   December 2026, because 1,713 days had passed but only 53,280 blocks.\n\t   Seconds first, blocks only for a claim whose verdict predates the stamp,\n\t   so a record keeps the deadline it was decided under. */\n\tif passed, known := pastDeadline(cs.verdictAtTime, finalizeGraceSecs); ((known \u0026\u0026 !passed) ||\n\t\t(!known \u0026\u0026 now \u003c cs.verdictAt+finalizeGraceBlocks)) \u0026\u0026 !isParticipant(cs, who) {\n\t\tpanic(\"kourtv2: OpenRewards is participant-only for its first week\")\n\t}\n\ttouch(c)\n\tadvancePools(c, cs) // settle the denominators to the freeze pin, exactly\n\n\tcs.rewardsOpened = true\n\tstripExit(c, cs)              // no longer policeable (§5.3 membership exit)\n\treleaseMetaLatchIfMeta(c, cs) // an appeal's review slot is freed at its end\n\t// The F9 time-averaged-stake cap divides by the claim's stakeable life. The\n\t// polish window is dead time (staking is closed), so counting it would\n\t// over-tighten every cap by (delay/life) — re-anchored (MODERATION.md §9).\n\t// Default delay 0 → identical to before.\n\tcs.openBlocks = cs.frozenAt - (cs.openedAt + c.params.stakeOpenDelayBlocks)\n\tif cs.openBlocks \u003c 1 {\n\t\tcs.openBlocks = 1\n\t}\n\n\t// The winning pool's conviction in CC units: the winner denominator AND\n\t// the mid-weight gross (the tier-invariant carrot base).\n\tvar midGross int64\n\tif int(cs.provisional) == sideYES {\n\t\tmidGross = convToCC(cs.yesConvHi, cs.yesConvLo)\n\t} else {\n\t\tmidGross = convToCC(cs.noConvHi, cs.noConvLo)\n\t}\n\tcs.winPoolConvCC = midGross\n\n\t// D = min(tier × midGross, G_MAX, R): the per-claim clamp is one period's\n\t// budget (P8), then the junior reservation clamps to the reservoir —\n\t// never aborts (F4).\n\t//\n\t// INTENTIONAL (econ-vet P7): D is the WHOLE claim draw and the 80/8/5 split\n\t// is taken OUT of it — so a winner receives 80/93 = 0.860·midGross, NOT the\n\t// full midGross with author/answerer added on top. PLAN §3.5's \"D = Σg·93/80\"\n\t// prose describes that gross-up; the code deliberately does NOT do it. The\n\t// code is ~16% more conservative (lower emission per claim, ceiling-safer,\n\t// and it worsens every mill break-even q*). Do NOT \"fix\" toward §3.5 — it\n\t// would inflate every draw 16%. Consequence, accepted: the honest sole-staker\n\t// break-even is p ≈ 0.68, not the 0.59 an un-split rate would give.\n\t// THE MULTIPLIER IS THE CLAIM'S OWN SIZE now, not a voted band (tier.gno).\n\t// It was `mustMul(cs.tier, midGross)` with tier in {0,1,2}; it is a\n\t// continuous bps multiplier over the same range, read from xBarFrozen.\n\t//\n\t// mulDiv128, not mustMul: midGross is CONVICTION — stake × time, not\n\t// supply-bounded — and the same warning the carrot's ×7 carries applies to a\n\t// ×2 here at extreme scale.\n\t// TWO MULTIPLIERS, COMPOSED. Size says what the claim was worth by the money\n\t// that showed up; spam says what the voters made of it, on the one round that\n\t// decided. Composed rather than either-or, so a dispute can only ever lower a\n\t// claim — see spamNetBps for why replacing would let a small self-staked claim\n\t// buy a promotion with a dispute bond.\n\t//\n\t// Floored twice, once per step, which is conservative in the only direction\n\t// that matters: the draw is never rounded up.\n\twant := drawWant(c, cs, midGross)\n\td := reserveJunior(c, want)\n\tcs.drawWinners, cs.drawAuthor, cs.drawAnswerer = drawSplit(cs, d)\n\t// THE AUTHOR'S STANDING CREDIT IS GONE, and it is worth saying why rather\n\t// than leaving a hole. It paid only at tier HIGH — \"a default MID is what an\n\t// unlooked-at claim gets, and crediting that is the author-mill's habitat\"\n\t// (PLAN §3.4 F1) — so with no tier there is no non-mill population left to\n\t// pay it to. Crediting every author instead would be the mill faucet the\n\t// gate existed to close, so the row is dropped rather than widened.\n\t//\n\t// The MONEY lane is untouched: AuthorBonus was never tier-gated, and an\n\t// author still draws cs.drawAuthor. What is gone is the standing ROW; the\n\t// standingCatAuthor slot stays in the persisted enum (append-only wire) and\n\t// simply stops receiving.\n\n\t// The carrot: 7 points of the MID-weight gross, senior-lane, per-voter clamped\n\t// at pull (P2). 128-bit: midGross is conviction (stake×time), not\n\t// supply-bounded, so a bare ×7 could wrap and then panic PullCarrot at extreme\n\t// scale (math-audit NOTE-2).\n\t//\n\t// NO LONGER WITHHELD ON TURNOUT. A conclusive quality tally below the\n\t// supply-floored fullBar used to zero this pot, because such a tally could only\n\t// be a LOW, ResolveFlag returned the flag bond WHOLE there, and the result was a\n\t// mint against ~1.1 CC of burn on never-escrowed weight — v0.25's A21 faucet\n\t// arriving through the carrot. Both halves of that vector are gone: there is no\n\t// quality tally to go conclusive below a bar, and no flag bond to return whole.\n\t// The carrot now pays only the deciding DISPUTE round's with-verdict voters, who\n\t// reached it by someone posting a dispute bond.\n\tcs.carrotTotal = mulDiv128(midGross, splitCarrot, 100)\n\tcs.carrotPool = cs.carrotTotal\n\n\t// N1 lands here: the deposit refunds unless a conclusive low already\n\t// burned it; the fee likewise. Exactly-once — both zero on disposition.\n\tif cs.deposit \u003e 0 {\n\t\tc.coin.Transfer(c.escrow, cs.author, cs.deposit)\n\t\tc.deposit -= cs.deposit\n\t\tcs.deposit = 0\n\t}\n\tif cs.fee \u003e 0 {\n\t\tc.coin.Transfer(c.escrow, cs.author, cs.fee)\n\t\tc.deposit -= cs.fee\n\t\tcs.fee = 0\n\t}\n\n\t// The slash reserve retained at SettleUndisputed (M3-HIGH-1) returns to the\n\t// answerer now if no conclusive-low slash consumed it. pendingSlash is 0\n\t// here (the gate above refuses while a slash is in its counter window), so\n\t// whatever remains in answerBond is un-slashed and owed back, exactly once.\n\tif cs.answerBond \u003e 0 {\n\t\tsettleAnswerBond(c, cs)\n\t}\n}\n\n// WithdrawBonus pays a winning-side staker their accuracy reward: their share\n// of the winners' slice, scaled by conviction then capped at (tier/2) × their\n// time-averaged stake (F9 — flash-proof base). One shot per position.\nfunc WithdrawBonus(cur realm, courtSlug string, claimID uint64, side int) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tmustSide(side)\n\tif !cs.rewardsOpened {\n\t\tpanic(\"kourtv2: the draw has not rewardsOpened\")\n\t}\n\tif side != int(cs.provisional) {\n\t\tpanic(\"kourtv2: only the winning side draws (principal came back either way)\")\n\t}\n\tv := cs.stakers.Get(posKey(who, side))\n\tif v == nil {\n\t\tpanic(\"kourtv2: no position on that side\")\n\t}\n\tp := v.(*stakePos)\n\tif p.bonusPaid {\n\t\tpanic(\"kourtv2: this position's bonus was already drawn\")\n\t}\n\ttouch(c)\n\t// SETTLE TO THE FREEZE PIN, and it is load-bearing twice over: the draw below\n\t// reads p.convHi/convLo, and creditWinConviction reads p.rawHi/rawLo. Neither\n\t// accrues for itself. advancePools settles the SIDE integrals only, so\n\t// without this line both read a figure stale as of this position's last stake\n\t// event — measured at ~7.9x understatement when that was last found.\n\taccrue(c, cs, p, side)\n\tp.bonusPaid = true\n\n\tconvCC := convToCC(p.convHi, p.convLo)\n\t// Standing for winning conviction (standing.gno): linear and uncapped, taken\n\t// from the SAME convCC the draw is computed from so the two can never\n\t// disagree about what was held.\n\t//\n\t// BEFORE THE CAP is the placement that matters: capBonus is concave, and a\n\t// concave credit pays a split position more than a whole one — the lesson\n\t// the carrot's own P2 per-voter clamp note records a few lines above.\n\t//\n\t// Before the early return below is NOT load-bearing, and an earlier version\n\t// of this comment implied it was. That return fires only when convCC \u003c= 0 or\n\t// the winning pool is empty, the second cannot happen while the first does\n\t// not (the pool integral contains this position's), and a zero credit is\n\t// refused downstream anyway — so either side of it behaves identically. It\n\t// reads better here; it defends nothing here.\n\tcreditWinConviction(c, cs, who, p)\n\tif convCC \u003c= 0 || cs.winPoolConvCC \u003c= 0 {\n\t\treturn 0\n\t}\n\tgross := mulDiv128(cs.drawWinners, convCC, cs.winPoolConvCC)\n\tpay := capBonus(cs, gross, p.rawHi, p.rawLo)\n\tmintEmission(c, who, pay)\n\treturn pay\n}\n\n// AuthorBonus and AnswererBonus are the 8- and 5-point one-shot slices, each\n// under its own (tier/2) cap: the author against their winning-side\n// time-averaged stake (F1), the answerer against the posted bond (v0.8).\nfunc AuthorBonus(cur realm, courtSlug string, claimID uint64) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif !cs.rewardsOpened {\n\t\tpanic(\"kourtv2: the draw has not rewardsOpened\")\n\t}\n\tif who != cs.author {\n\t\tpanic(\"kourtv2: only the author draws this slice\")\n\t}\n\tif cs.authorPaid {\n\t\tpanic(\"kourtv2: the author slice was already drawn\")\n\t}\n\ttouch(c)\n\tcs.authorPaid = true\n\tvar pay int64\n\t// I5, second gate: a seeded claim pays its author nothing here either. The\n\t// draw is already zeroed at open the rewards, so this is belt-and-braces — but\n\t// the invariant is asserted at the payout site because that is where a\n\t// future change would most plausibly break it.\n\tif cs.seeded {\n\t\tmintEmission(c, who, 0)\n\t\treturn 0\n\t}\n\tif v := cs.stakers.Get(posKey(who, int(cs.provisional))); v != nil {\n\t\tp := v.(*stakePos)\n\t\taccrue(c, cs, p, int(cs.provisional))\n\t\tpay = capBonus(cs, cs.drawAuthor, p.rawHi, p.rawLo)\n\t}\n\tmintEmission(c, who, pay)\n\treturn pay\n}\n\nfunc AnswererBonus(cur realm, courtSlug string, claimID uint64) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif !cs.rewardsOpened {\n\t\tpanic(\"kourtv2: the draw has not rewardsOpened\")\n\t}\n\tif who != cs.answerer {\n\t\tpanic(\"kourtv2: only the answerer draws this slice\")\n\t}\n\tif cs.answererPaid {\n\t\tpanic(\"kourtv2: the answerer slice was already drawn\")\n\t}\n\ttouch(c)\n\tcs.answererPaid = true\n\tpay := cs.drawAnswerer\n\t// tier/2 × the bond, as it always was — now a bps multiplier. Both operands\n\t// are plain int64 here (a bond, not an integral), but mulDiv128 keeps the\n\t// rounding identical to the old `mustMul(...) / 2`: multiply first, halve\n\t// once, floor once.\n\tif bound := mulDiv128(cs.answerBond0, tierBpsFor(cs), 2*tierParBps); pay \u003e bound {\n\t\tpay = bound\n\t}\n\tmintEmission(c, who, pay)\n\treturn pay\n}\n\n// capBonus applies the F9 cap: (tier/2) × time-averaged stake, where the\n// average is the raw ∫stake·dt over the claim's open life. Floors.\n// timeAvgStake is a position's raw integral divided by the claim's open blocks —\n// the F9/Q7 cap base, and now also the base the author and conviction STANDING\n// rows are scaled on (standing.gno §2.2).\n//\n// EXTRACTED RATHER THAN COPIED, because the second caller arrived and the divide\n// was folded inside capBonus together with the (tier/2) multiply. Two spellings\n// of one quantity is how the two come to disagree.\n//\n// AND IT CLAMPS WHERE div128 PANICS. div128 aborts on `hi \u003e= den`, which is fine\n// where it has always run — inside the pull functions, where a panic strands one\n// address. The standing credits run inside OpenRewards, where the same panic\n// would strand the whole claim's draw: the exact blast-radius widening satAdd\n// and scaleBps exist to prevent on these paths. Unreachable at real magnitudes;\n// clamped anyway, because \"unreachable\" is what every one of those guards was\n// also told about itself.\nfunc timeAvgStake(cs *claimState, rawHi, rawLo uint64) int64 {\n\tif cs.openBlocks \u003c= 0 || rawHi \u003e= uint64(cs.openBlocks) {\n\t\treturn 0\n\t}\n\treturn div128(rawHi, rawLo, cs.openBlocks)\n}\n\nfunc capBonus(cs *claimState, gross int64, rawHi, rawLo uint64) int64 {\n\trawAvg := timeAvgStake(cs, rawHi, rawLo)\n\t// Was `mustMul(cs.tier, rawAvg) / 2` — the F9/Q7 cap, in bps now.\n\tbound := mulDiv128(rawAvg, tierBpsFor(cs), 2*tierParBps)\n\tif gross \u003e bound {\n\t\tgross = bound\n\t}\n\tif gross \u003c 0 {\n\t\tgross = 0\n\t}\n\treturn gross\n}\n\n// PullCarrot claims a voter's participation carrot: with-verdict dispute\n// voters when a decided round resolved the claim, else all non-participant\n// quality voters of the conclusive standalone tally (P14). Weight-pro-rata,\n// clamped per voter at b₀/2 − 1 (P2 — an idle sybil voting to farm the carrot\n// must net strictly negative against its own flag bond), senior-queued\n// (2A-T1: policing pay is never scaled, only delayed).\nfunc PullCarrot(cur realm, courtSlug string, claimID uint64) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif !cs.rewardsOpened {\n\t\tpanic(\"kourtv2: the draw has not rewardsOpened\")\n\t}\n\tif isParticipant(cs, who) {\n\t\tpanic(\"kourtv2: participants take their own slices, never the carrot\")\n\t}\n\tif cs.voted == nil {\n\t\tpanic(\"kourtv2: nobody voted on this claim\")\n\t}\n\t// THE CARROT PAYS THE WEIGHT THAT WAS TALLIED, read back from THIS REALM's own\n\t// record of the vote — never re-derived, and never from the governor.\n\t//\n\t// Two reasons, and the second is a bug this file briefly had. Re-deriving from\n\t// PastVotes is exactly equal only while the tally is a bare snapshot read, and\n\t// IT NO LONGER IS: every lane now caps at the voter's live balance, so a\n\t// re-derived numerator would exceed that voter's own contribution to `denom`\n\t// and the carrot would over-pay everyone who was capped. What was a precaution\n\t// when written is load-bearing now (VOTEFLOOR.md). And reading it from the governor is worse\n\t// than either: ReleaseRoll is permissionless and sets p.voted = nil, so\n\t// gov.VoteOf stops answering once anyone reclaims the roll, and a claimant who\n\t// had not pulled yet would be refused a carrot they had earned. dispute.gno\n\t// has said so since the choice record was added; an earlier version of this\n\t// block ignored it and called VoteOf directly.\n\tif cs.decidedPID == 0 {\n\t\tpanic(\"kourtv2: no vote resolved this claim; there is no carrot\")\n\t}\n\tv := cs.voted.Get(\"d\" + beClaimKey(uint64(cs.decidedPID)) + string(who))\n\tif v == nil || v.(string) != cs.carrotChoice {\n\t\tpanic(\"kourtv2: the carrot pays with-verdict voters of the deciding round\")\n\t}\n\tvw := cs.voted.Get(\"dw\" + beClaimKey(uint64(cs.decidedPID)) + string(who))\n\tif vw == nil {\n\t\tpanic(\"kourtv2: no recorded weight for that voter on the deciding round\")\n\t}\n\tw, denom := vw.(int64), cs.carrotDenom\n\t// KEYED ON THE DECIDING ROUND, and this line is the one to get right: it is\n\t// the only thing standing between a voter and pulling the carrot twice.\n\t//\n\t// It used to key on cs.conclusiveSeq, which was the QUALITY tally's sequence\n\t// number. On a dispute-decided claim that field was 0, so the key was \"c\" +\n\t// eight zero bytes + the address — a correct per-address latch by accident of\n\t// the constant rather than by design. conclusiveSeq is gone with the quality\n\t// lane, so the latch is re-keyed deliberately onto the round that actually\n\t// paid, which is the same round the weight above was read from.\n\tclaimedKey := \"c\" + beClaimKey(uint64(cs.decidedPID)) + string(who)\n\tif cs.voted.Has(claimedKey) {\n\t\tpanic(\"kourtv2: carrot already claimed\")\n\t}\n\tcs.voted.Set(claimedKey, true)\n\tif w \u003c= 0 || denom \u003c= 0 {\n\t\treturn 0\n\t}\n\ttouch(c)\n\tshare := mulDiv128(cs.carrotTotal, w, denom)\n\tb0 := cs.xBarFrozen * carrotClampXBps / 10000\n\tif b0 \u003c carrotClampMinCC {\n\t\tb0 = carrotClampMinCC\n\t}\n\tif clamp := b0/2 - 1; share \u003e clamp {\n\t\tshare = clamp // P2: the sybil-margin invariant, per voter\n\t}\n\tif share \u003e cs.carrotPool {\n\t\tshare = cs.carrotPool\n\t}\n\tif share \u003c= 0 {\n\t\treturn 0\n\t}\n\tcs.carrotPool -= share\n\tenqueueSenior(c, who, share, \"carrot\")\n\treturn share\n}\n\n// div128 floors a u128 by a positive int64 divisor into int64.\nfunc div128(hi, lo uint64, den int64) int64 {\n\tif den \u003c= 0 {\n\t\tpanic(\"kourtv2: div128 domain\")\n\t}\n\tif hi \u003e= uint64(den) {\n\t\tpanic(\"kourtv2: div128 quotient overflow\") // unreachable at bounded magnitudes\n\t}\n\tq, _ := bits.Div64(hi, lo, uint64(den))\n\treturn int64(q)\n}\n\n// ------------------------------------------------------------------ reads --\n\nfunc RewardsOpened(courtSlug string, claimID uint64) bool {\n\treturn mustClaim(mustCourt(courtSlug), claimID).rewardsOpened\n}\n\n// drawWant is the draw a claim ASKS for, before the reservoir is consulted.\n//\n// A FUNCTION SO THE QUOTE AND THE PAYMENT CANNOT DISAGREE. DrawQuote below shows\n// a reader what pressing \"Open the rewards\" will pay, and a page deriving that\n// figure independently is the same defect as a page deriving a deadline\n// independently — which this realm has had three times. One computation, two\n// callers, and the cap's reasoning moved here with it, unchanged.\nfunc drawEntitlement(c *Court, cs *claimState, midGross int64) int64 {\n\ttierBps := tierBpsFor(cs)\n\twant := mulDiv128(midGross, tierBps, tierParBps)\n\twant = mulDiv128(want, spamNetBps(cs), tierParBps)\n\t// DRAW CAP: the claim may not mint more than the answer bond collateralized.\n\t//\n\t// WHY. A sniper stakes dust on one side, declares it, and lets 72h of silence\n\t// settle: the opposing pool's whole draw is annihilated while the bond returns\n\t// whole. What that costs the attacker is bounded by the bond; what it destroys\n\t// is bounded by tier × midGross. So the ratio destroyed/posted — the only\n\t// well-defined leverage here, since on every realized path either destruction\n\t// or forfeiture is zero — is (tier + carrot)/(bondFloorConvBps/1e4). That is\n\t// 1.07/1.6 = 0.669 at MID but 2.07/1.6 = 1.294 at HIGH, i.e. sniping an\n\t// exceptional claim PAYS. Measured 1.2937 end to end.\n\t//\n\t// The alternative was to freeze the tier the floor was sized against. It is\n\t// strictly dominated: because answerBond0 \u003e= 1.6·mg always, capd \u003e= 1.53·mg,\n\t// so the cap never pays less than 1.53×mg where the freeze always pays 1×mg —\n\t// the freeze is this cap evaluated at the extremal and then applied\n\t// everywhere, abolishing the HIGH premium (0% kept) where the cap keeps 53%.\n\t// A stored frozen-tier field would also be dead state: slashSizeAt has no tier\n\t// factor, so the value is tierMidX on every claim.\n\t//\n\t// STANDING ANSWERS ONLY, and this gate is load-bearing rather than tidy. The\n\t// leverage above compares what a snipe destroys against what the sniper\n\t// FORFEITS, and on an overturn the bond has already burned in full\n\t// (dispute.gno, A19) — the collateral premise is satisfied by the forfeiture,\n\t// so there is nothing left for a cap to bound. Capping there would instead cut\n\t// the vindicated majority's draw to pay for an attack that already failed, and\n\t// it is the one path where the floor carries no information about the winning\n\t// pool: the floor keys on the ANSWERED side, which on an overturn is the losing\n\t// side. Measured on a dust-answered overturn, the floor delivered 40% of the\n\t// 1.6·mg_win it is casually assumed to give.\n\t//\n\t// INERT ON THE PUBLISHED RATE. Two regimes, and BOTH are needed — an earlier\n\t// version of this comment gave only the first and was therefore wrong about the\n\t// shipped tree:\n\t//\n\t//   floor binds (mg_max \u003e= answerBondBps/bondFloorConvBps·X̄):\n\t//     capd \u003e= (1.60 - 0.07)·mg_win = 1.53·mg_win \u003e want. Carries no X̄, no age,\n\t//     no rate, no bond level — this is the post-re-keying guarantee.\n\t//   base arm binds (mg_max below that):\n\t//     capd \u003e= answerBondBps·X̄ - 0.07·mg_max, and mg_max is small by assumption,\n\t//     so the slack is answerBondBps·X̄ minus a fraction of something smaller.\n\t//     At 600 bps: 573 bps·X̄ against a want under 375 bps·X̄.\n\t//\n\t// On the CURRENT tree it is the second regime that carries it — the 5000 bps\n\t// base arm against maxMidGrossBps, measured at 2.42× margin. That is a\n\t// calibration, not a proof — exactly what court.gno's own \"CALIBRATION, NOT A\n\t// SAFETY PROOF\" note says of the neighbouring draw-arm check. **So the bond constant must not drop to 600 before\n\t// the floor is re-keyed to max over both sides. The two deploy\n\t// interlocks (\"answer bond below the undisputed-extraction bound\" and \"the draw\n\t// arm exceeds the base bond\") force that change to be deliberate, but they do\n\t// not force the two halves into one commit, and this cap is a third reason they\n\t// must be.**\n\t//\n\t// Do NOT lower openrewards.gno's AnswererBonus cap or capBonus to match:\n\t// lowering this arm alone can only make those LESS binding, and lowering them\n\t// independently strands coin.\n\tif int(cs.provisional) == int(cs.answer) {\n\t\tif capd := cs.answerBond0 - mulDiv128(midGross, splitCarrot, 100); want \u003e capd {\n\t\t\twant = capd\n\t\t\tif want \u003c 0 {\n\t\t\t\twant = 0\n\t\t\t}\n\t\t}\n\t}\n\treturn want\n}\n\n// drawWant is drawEntitlement clamped by what this period may still mint. The\n// clamp lives HERE and not in the entitlement because it is a property of the\n// court's emission schedule at the moment of payment, not of what the claim\n// earned — and DrawQuote must not pretend to know the first.\nfunc drawWant(c *Court, cs *claimState, midGross int64) int64 {\n\twant := drawEntitlement(c, cs, midGross)\n\tif want \u003e c.curPeriodBudget {\n\t\twant = c.curPeriodBudget\n\t}\n\treturn want\n}\n\n// drawSplit cuts a granted draw into its three slices. Shared with DrawQuote for\n// the same reason as drawWant: the figure a reader is shown and the figure they\n// are paid have to come from one place. The two exclusions and their reasoning\n// travel with it.\nfunc drawSplit(cs *claimState, d int64) (winners, author, answerer int64) {\n\twinners = d * splitWinners / (splitWinners + splitAuthor + splitAnswerer)\n\tauthor = d * splitAuthor / (splitWinners + splitAuthor + splitAnswerer)\n\tanswerer = d * splitAnswerer / (splitWinners + splitAuthor + splitAnswerer)\n\t// An OVERTURNED answer never stood — the answerer was adjudicated wrong\n\t// (bond burned in full, record reset). They draw NO answerer slice, mirroring\n\t// the author slice's winning-side gate and PLAN Appendix B's overturn example\n\t// (audit-final MED). The unclaimed points stay unminted — conservative, under\n\t// the ceiling. The undisputed and upheld paths set provisional == answer, so\n\t// a standing answer is paid as before.\n\tif int(cs.provisional) != int(cs.answer) {\n\t\tanswerer = 0\n\t}\n\t// I5 (MODERATION.md §8): a moderator-SEEDED claim never pays an author\n\t// slice. Seeding waives the CC deposit/fee to bootstrap a young court, and\n\t// the author slice staying unminted is what proves seeding is uncompensated\n\t// work — the securities-relevant fact, and it keeps the waiver un-farmable.\n\t// The points stay unminted (conservative, under the ceiling).\n\tif cs.seeded {\n\t\tauthor = 0\n\t}\n\t// A PROPOSAL PAYS ITS AUTHOR AND NOBODY ELSE. See claimState.noAccuracy: the\n\t// accuracy slice pays stakers by conviction for backing the right side of a\n\t// question of FACT, and a `mod:newset` appeal is not one. The author slice\n\t// stays, because writing a proposal the court adopts is the work being\n\t// rewarded. Unclaimed points stay unminted, like every other zeroed slice\n\t// here — conservative, under the ceiling.\n\tif cs.noAccuracy {\n\t\twinners = 0\n\t}\n\treturn winners, author, answerer\n}\n\n// DrawQuote is what OpenRewards would pay if it ran now: the three slices, in CC.\n//\n// (0,0,0) on a claim with no final verdict or one already rewardsOpened — in the\n// second case DrawSlices holds the real figures, and answering here too would\n// only invite a reader to compare two answers to one question.\n//\n// WHAT THE CLAIM EARNED, and the payment can only be this or less. The two\n// clamps left off are the court's, not the claim's: the period budget and the\n// junior reservoir are both advanced by touch(), which reserveJunior calls and a\n// READ MAY NOT (check-read-purity — a read that mutated would let the first\n// caller change what the second sees). Quoting them untouched reports 0 on a\n// court nobody has poked this period, which is worse than not quoting them: a\n// page would tell a reader their reward is nothing when it is not.\n//\n// So this answers \"what has this claim earned\", and the reservoir figure the\n// court page already shows answers \"what can be paid out right now\". Two honest\n// numbers beat one dishonest one, and the tier/spam/bond chain that decides the\n// first is shared with the payment path so it cannot drift.\nfunc DrawQuote(courtSlug string, claimID uint64) (winners, author, answerer int64) {\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif cs.verdictAt == 0 || cs.rewardsOpened || cs.provisional \u003c 0 {\n\t\treturn 0, 0, 0\n\t}\n\tvar midGross int64\n\tif cs.provisional == sideYES {\n\t\tmidGross = convToCC(cs.yesConvHi, cs.yesConvLo)\n\t} else {\n\t\tmidGross = convToCC(cs.noConvHi, cs.noConvLo)\n\t}\n\treturn drawSplit(cs, drawEntitlement(c, cs, midGross))\n}\n\n// DrawSlices reads the rewardsOpened slice budgets (winners, author, answerer)\n// and the carrot pool remaining — the \"what is left to pull\" legibility line.\nfunc DrawSlices(courtSlug string, claimID uint64) (winners, author, answerer, carrotLeft int64) {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\treturn cs.drawWinners, cs.drawAuthor, cs.drawAnswerer, cs.carrotPool\n}\n\n// PullState reports which reward slices `who` has already pulled on a\n// rewardsOpened claim — the winning-side accuracy bonus (their position's\n// one-shot flag), the author slice, and the answerer slice — so a UI can grey\n// out spent pulls. All false before open the rewards / for non-participants.\nfunc PullState(courtSlug string, claimID uint64, who address) (winnerPaid, authorPaid, answererPaid bool) {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tif cs.provisional \u003e= 0 {\n\t\tif v := cs.stakers.Get(posKey(who, int(cs.provisional))); v != nil {\n\t\t\twinnerPaid = v.(*stakePos).bonusPaid\n\t\t}\n\t}\n\treturn winnerPaid, cs.authorPaid \u0026\u0026 who == cs.author, cs.answererPaid \u0026\u0026 who == cs.answerer\n}\n"},{"name":"posting.gno","body":"// Posting: what a level is, what it costs to reach, and how fast it lets you\n// write (docs/CLAIM_BOARDS.md §3 and §4). standing.gno owns the RECORD; this file\n// owns what the record buys.\n//\n// TWO DOORS, AND NO FREE TIER. You post at a level, and you reach one of two\n// ways: earn standing, or burn CC once for an entry pass. Zero standing and no\n// pass is zero posts — not a small allowance, zero. Any free per-address\n// allowance is a free sybil flood, which is the whole reason the door is priced\n// at all.\n//\n// THE THRESHOLDS ARE FRACTIONS OF SUPPLY, never absolute standing. Standing is\n// denominated in CC and a mature court's acts pay on the order of a hundred times\n// a young court's for the same work, so a fixed number is unreachable on one and\n// trivial on the other. Every other bar in this realm is already a fraction of\n// supply for the same reason — quorumSupplyBps, electionFloor, supplyFloor — and\n// this one reads the same sealed epoch they do, because live supply is griefable\n// in the same block.\n\npackage kourtv2\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"strings\"\n\n\tsanitize \"gno.land/p/nt/markdown/sanitize/v0\"\n)\n\n// The ladder, in bps of supply, and the rate each rung buys.\n//\n// The TOP-TO-ENTRY RATIO IS A REALM CONSTANT and the rungs are not: the ratio is\n// what bounds both a splitter's gain and what farming standing is worth in\n// throughput, so a court that raised it would move a bound that protects readers\n// of its own board and everyone reasoning about the lane. The thresholds only\n// shape how fast people climb inside one court.\nconst (\n\tladderT1BpsInit = int64(5)   // 0.05% of supply — about one undisputed win\n\tladderT2BpsInit = int64(25)  // 0.25% — five wins, or one caught bad claim\n\tladderT3BpsInit = int64(100) // 1% — twenty wins, or three caught bad claims\n\n\tpostsEntryInit = int64(3)  // level 1: the pass, or t1 standing\n\tpostsT2Init    = int64(10) // level 2\n\tpostsT3Init    = int64(30) // level 3 — ten times entry, the frozen ratio\n\n\t// ladderTopRatio is that ratio, checked at deploy. Frozen: see above.\n\tladderTopRatio = int64(10)\n\n\t// passPriceMinCC floors the entry burn so a young court is never free, and\n\t// passPriceBps scales it so it does not decay to nothing as a court grows.\n\t// The EFFECTIVE price is additionally lidded — see passPriceFor, where the\n\t// lid is the §16 relation rather than a number.\n\tpassPriceMinCC = int64(100_000) // 0.1 CC\n\tpassPriceBps   = int64(1)       // 0.01% of supply\n\n\t// passPriceOfT3 is that relation: a unit of standing must be worth little\n\t// enough that the flag lane's burn still dominates it (A19) and staking does\n\t// not pay on carry alone. Both reduce to V \u003c= 0.10, and substituting the\n\t// substitutability bound V \u003c= ratio x passPrice / t3 gives t3 \u003e= 100 x\n\t// passPrice. docs/CLAIM_BOARDS.md §16 carries the derivation.\n\tpassPriceOfT3 = int64(100)\n)\n\n// mustPostingInvariants is the deploy gate on the frozen half.\n//\n// ITS PANICS ARE IN NO MUTATION BATCH, deliberately, and for the reason\n// mustElectionInvariants records about its own: every comparison here is between\n// package CONSTANTS, and init() runs on every package load including every test\n// load, so the relationships are enforced on every run by the guard itself.\n// Violate one and the package does not load; delete a panic and nothing can\n// observe the difference, because the constants satisfy it.\nfunc mustPostingInvariants() {\n\tif postsT3Init != postsEntryInit*ladderTopRatio {\n\t\tpanic(\"kourtv2: the top rung must be exactly ladderTopRatio x entry — the ratio is the bound, not a suggestion\")\n\t}\n\tif !(0 \u003c ladderT1BpsInit \u0026\u0026 ladderT1BpsInit \u003c ladderT2BpsInit \u0026\u0026 ladderT2BpsInit \u003c ladderT3BpsInit) {\n\t\tpanic(\"kourtv2: the ladder must be strictly increasing\")\n\t}\n\tif !(0 \u003c postsEntryInit \u0026\u0026 postsEntryInit \u003c postsT2Init \u0026\u0026 postsT2Init \u003c postsT3Init) {\n\t\tpanic(\"kourtv2: the rates must be strictly increasing\")\n\t}\n\t// The §16 relation, on the DEFAULTS. passPriceFor clamps it at runtime for\n\t// every court and every supply; this catches a default that could never\n\t// satisfy it.\n\tif ladderT3BpsInit \u003c passPriceOfT3*passPriceBps {\n\t\tpanic(\"kourtv2: t3 must be at least 100x the pass price, or a unit of standing is worth more than the flag lane's burn can cover\")\n\t}\n}\n\nfunc init() { mustPostingInvariants() }\n\n// ---- the ladder: a realm default a court may override ---------------------\n\n// ladder is the five settable numbers. A TUPLE, as creditRates is, because the\n// bound on t3 is a RELATION to the pass price and the bound on the rungs is that\n// they increase — neither is a range on one number, and independent setters would\n// leave a window between two calls where the relation is violated.\ntype ladder struct {\n\tt1Bps, t2Bps, t3Bps int64\n\tpostsEntry, postsT2 int64\n}\n\nvar ladderDefault = ladder{\n\tt1Bps: ladderT1BpsInit, t2Bps: ladderT2BpsInit, t3Bps: ladderT3BpsInit,\n\tpostsEntry: postsEntryInit, postsT2: postsT2Init,\n}\n\n// postsT3 is derived, never stored: the ratio is frozen, so the top rung is not\n// a settable number and cannot drift from the constant that bounds it.\nfunc (l ladder) postsT3() int64 { return l.postsEntry * ladderTopRatio }\n\nfunc ladderFor(c *Court) ladder {\n\tif c.ladder != nil {\n\t\treturn *c.ladder\n\t}\n\treturn ladderDefault\n}\n\n// mustLadder refuses a set that breaks the ordering or the §16 relation.\n//\n// t1 AND t2 CARRY NO CROSS-PARAMETER CONDITION, and t3 does. Getting t1 or t2\n// wrong only changes how fast people climb inside one court, which is\n// self-punishing — a slow board or a loose one, harming nobody who did not\n// choose it. Getting t3 wrong changes what a unit of standing is WORTH, which is\n// the bound the whole earning table rests on. Hence one extra clause on one\n// number, and it is a relation rather than a range.\nfunc mustLadder(l ladder) {\n\tif !(0 \u003c l.t1Bps \u0026\u0026 l.t1Bps \u003c l.t2Bps \u0026\u0026 l.t2Bps \u003c l.t3Bps \u0026\u0026 l.t3Bps \u003c= 10_000) {\n\t\tpanic(\"kourtv2: the ladder must be strictly increasing within 0..10000 bps\")\n\t}\n\tif !(0 \u003c l.postsEntry \u0026\u0026 l.postsEntry \u003c l.postsT2 \u0026\u0026 l.postsT2 \u003c l.postsT3()) {\n\t\tpanic(\"kourtv2: the posting rates must be strictly increasing\")\n\t}\n\tif l.t3Bps \u003c passPriceOfT3*passPriceBps {\n\t\tpanic(\"kourtv2: t3 must stay at least 100x the pass price — lowering it alone would make a unit of standing worth more than the flag lane's burn can cover\")\n\t}\n}\n\n// SetLadderDefault sets the realm-wide ladder every court inherits.\nfunc SetLadderDefault(cur realm, t1Bps, t2Bps, t3Bps, postsEntry, postsT2 int64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\td := ensureGlobalDAO()\n\tif cur.Previous().Address() != d.admin {\n\t\tpanic(\"kourtv2: only the global DAO admin sets the default ladder\")\n\t}\n\tl := ladder{t1Bps, t2Bps, t3Bps, postsEntry, postsT2}\n\tmustLadder(l)\n\tladderDefault = l\n\t// EMITTED, because this is the widest act in the lane: it moves the\n\t// default for EVERY court at once. The per-court setters log and emit;\n\t// these two changed the floor under all of them in silence. There is no\n\t// single court to log against, so the event is the whole record.\n\tchain.Emit(globalActEvent,\n\t\t\"court\", \"*\",\n\t\t\"claim\", \"0\",\n\t\t\"act\", \"set-ladder-default:\"+ladderKey(l),\n\t\t\"by\", cur.Previous().Address().String(),\n\t\t\"height\", eventHeight(),\n\t)\n}\n\n// SetCourtLadder overrides the posting ladder for one court, replacing the\n// default. ClearCourtLadder puts it back.\n//\n// t1Bps, t2Bps and t3Bps are the standing thresholds for posting levels 1, 2 and\n// 3, in basis points of the court's sealed supply. postsEntry and postsT2 are\n// posts per day at the entry rung and at level 2; the level-3 rate is derived\n// from postsEntry and is not settable.\n//\n// SEVERAL MODERATORS MUST AGREE, at the court's speech threshold, because this\n// is the widest speech lever there is: one bad set of numbers throttles every\n// poster in the court at once. The proposed VALUES are part of what is agreed,\n// so two moderators proposing different ladders do not accumulate toward one\n// threshold. The call returns without effect until the threshold is met. It is\n// logged, evented, and the global DAO can undo it with GlobalClearCourtParams.\n//\n// Refused when the court does not exist, the caller is not an active moderator,\n// the three thresholds are not strictly increasing within 1..10000 bps, the two\n// rates are not strictly increasing, or t3Bps drops below 100x the pass price —\n// which would make a unit of standing worth more than the flag lane's burn can\n// cover.\nfunc SetCourtLadder(cur realm, courtSlug string, t1Bps, t2Bps, t3Bps, postsEntry, postsT2 int64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\trequireActiveMod(cm, who)\n\tl := ladder{t1Bps, t2Bps, t3Bps, postsEntry, postsT2}\n\tmustLadder(l)\n\t// THE VALUES ARE IN THE ACTION KEY. Without them two moderators proposing\n\t// DIFFERENT ladders would each count toward one threshold, and the set would\n\t// install whichever one happened to arrive last — an m-of-n that never agreed\n\t// on anything.\n\tif fire, _ := approveAction(cm.pending, \"setladder:\"+ladderKey(l), who, \"\",\n\t\tspeechM(cm)); !fire {\n\t\treturn\n\t}\n\tc.ladder = \u0026l\n\tcm.appendCourtLog(who, \"setladder:\"+ladderKey(l), \"\")\n\temitModAct(c.id, 0, \"setladder\", who)\n}\n\n// ladderKey is the tuple as one string, for the action key and the log row. One\n// definition so the thing voted on and the thing recorded cannot differ.\nfunc ladderKey(l ladder) string {\n\treturn strconv.FormatInt(l.t1Bps, 10) + \":\" + strconv.FormatInt(l.t2Bps, 10) +\n\t\t\":\" + strconv.FormatInt(l.t3Bps, 10) + \":\" +\n\t\tstrconv.FormatInt(l.postsEntry, 10) + \":\" + strconv.FormatInt(l.postsT2, 10)\n}\n\n// ClearCourtLadder returns a court to the realm default.\n//\n// requireMod, not requireActiveMod: returning to the realm default is\n// de-escalation, and a suspended set must be able to undo its own throttle — the\n// same rule the six sanction undos follow, and the reason a suspended set could\n// not previously clear the very override that got it suspended.\nfunc ClearCourtLadder(cur realm, courtSlug string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\trequireMod(cm, who)\n\tif c.ladder == nil {\n\t\tpanic(\"kourtv2: this court has no ladder override\")\n\t}\n\tif fire, _ := approveAction(cm.pending, \"clearladder:\"+c.id, who, \"\",\n\t\tspeechM(cm)); !fire {\n\t\treturn\n\t}\n\tc.ladder = nil\n\tcm.appendCourtLog(who, \"clearladder\", \"\")\n\temitModAct(c.id, 0, \"clearladder\", who)\n}\n\n// ---- the price of the entry pass ------------------------------------------\n\n// passPriceFor is the CC an address burns once to hold the entry level.\n//\n// THE LID IS THE §16 RELATION, EXPRESSED AS A CLAMP RATHER THAN A CHECK, and\n// that is what makes the relation hold at every supply instead of only where a\n// setter looked. The floor exists so a young court is never free; on a court\n// small enough for the floor to bind it can exceed t3/100, and a check would\n// then have to refuse a configuration nobody chose. Clamping keeps the door\n// priced AND the bound intact, and the two never fight.\n//\n// Reads the last SEALED epoch, never live supply: supplyFloor's own note says\n// why — a live figure is griefable inside the same block.\n// boardSupply is the sealed supply every board threshold is quoted against, and\n// whether there IS one yet.\n//\n// grc20votes' mustBeSealed panics on `at == 0 || at \u003e= Epoch()`, so\n// PastTotal(Epoch()-1) panics for any court whose coin has not reached epoch 2 —\n// which is every brand-new court. The realm already lives with that on the\n// dispute and election paths (quorumFloor and electionBond read the same\n// expression), but those are unreachable until a claim has been answered and\n// disputed, by which time epochs have rolled. The board is reachable in the\n// first block, so it hit the condition head-on: `PostComment` aborted with\n// \"grc20votes: that epoch has not been sealed yet\", and `PassPrice`, `PostLevel`\n// and the whole `/me` page — all READS — panicked outright.\n//\n// Found by z_read_filetest on the first run after the board reads were added to\n// it, which is exactly the defect class that file exists for: every unit test\n// skips two epochs in its fixture (see postingFixture's own note), so the suite\n// could not see it.\nfunc boardSupply(c *Court) (int64, bool) {\n\tif c.coin.Epoch() \u003c 2 {\n\t\treturn 0, false\n\t}\n\treturn c.coin.PastTotal(c.coin.Epoch() - 1), true\n}\n\nfunc passPriceFor(c *Court) int64 {\n\tsup, sealed := boardSupply(c)\n\tif !sealed {\n\t\treturn 0\n\t}\n\tp := sup * passPriceBps / 10_000\n\tif p \u003c passPriceMinCC {\n\t\tp = passPriceMinCC\n\t}\n\tif lid := sup * ladderFor(c).t3Bps / 10_000 / passPriceOfT3; p \u003e lid {\n\t\tp = lid\n\t}\n\tif p \u003c 0 {\n\t\tp = 0\n\t}\n\treturn p\n}\n\n// PassPrice is what an entry pass costs in this court right now.\nfunc PassPrice(courtSlug string) int64 { return passPriceFor(mustCourt(courtSlug)) }\n\n// Ladder is what this court currently runs, packed:\n//\n//\tt1:\u003cbps\u003e;t2:\u003cbps\u003e;t3:\u003cbps\u003e;entry:\u003cn\u003e;l2:\u003cn\u003e;l3:\u003cn\u003e;inherited:\u003c0|1\u003e\nfunc Ladder(courtSlug string) string {\n\tc := mustCourt(courtSlug)\n\tl := ladderFor(c)\n\tinherited := \"1\"\n\tif c.ladder != nil {\n\t\tinherited = \"0\"\n\t}\n\treturn \"t1:\" + strconv.FormatInt(l.t1Bps, 10) +\n\t\t\";t2:\" + strconv.FormatInt(l.t2Bps, 10) +\n\t\t\";t3:\" + strconv.FormatInt(l.t3Bps, 10) +\n\t\t\";entry:\" + strconv.FormatInt(l.postsEntry, 10) +\n\t\t\";l2:\" + strconv.FormatInt(l.postsT2, 10) +\n\t\t\";l3:\" + strconv.FormatInt(l.postsT3(), 10) +\n\t\t\";inherited:\" + inherited\n}\n\n// ---- the level ------------------------------------------------------------\n\n// postLevel is what an address may do, 0..3.\n//\n// The pass floors at 1 and standing may lift above it, so the two doors compose\n// rather than compete: buying in does not cap what earning can reach, and\n// earning does not refund a pass.\nfunc postLevel(c *Court, who address) int64 {\n\tl := ladderFor(c)\n\tsup, sealed := boardSupply(c)\n\tif !sealed {\n\t\t// NOT \"level 0 because you have no standing\" — the court has no sealed\n\t\t// history to quote a threshold against yet. Nobody posts, pass or no\n\t\t// pass: a pass cannot be bought either (BuyCommentPass refuses), so\n\t\t// honouring one here would be honouring something unobtainable.\n\t\treturn 0\n\t}\n\tvar s int64\n\tif r := lookupStanding(c, who); r != nil {\n\t\ts = r.score\n\t}\n\tswitch {\n\tcase s \u003e= sup*l.t3Bps/10_000:\n\t\treturn 3\n\tcase s \u003e= sup*l.t2Bps/10_000:\n\t\treturn 2\n\tcase s \u003e= sup*l.t1Bps/10_000:\n\t\treturn 1\n\t}\n\tif r := lookupStanding(c, who); r != nil \u0026\u0026 r.passHeld {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// postsPerDayAt is the rate a level buys.\nfunc postsPerDayAt(l ladder, level int64) int64 {\n\tswitch level {\n\tcase 3:\n\t\treturn l.postsT3()\n\tcase 2:\n\t\treturn l.postsT2\n\tcase 1:\n\t\treturn l.postsEntry\n\t}\n\treturn 0\n}\n\n// PostLevel and PostsPerDay are the two figures a client needs to say what an\n// address may do, and why.\nfunc PostLevel(courtSlug string, who address) int64 {\n\treturn postLevel(mustCourt(courtSlug), who)\n}\n\nfunc PostsPerDay(courtSlug string, who address) int64 {\n\tc := mustCourt(courtSlug)\n\treturn postsPerDayAt(ladderFor(c), postLevel(c, who))\n}\n\n// ---- the entry pass: the one coin movement in this lane -------------------\n\n// BuyCommentPass burns the caller's own CC and floors their posting level in\n// this court at the entry rung, so they may post without first earning standing.\n// Returns the price burned, in CC base units.\n//\n// The price is what PassPrice(courtSlug) quotes at the moment of the call. The\n// coin is DESTROYED, not escrowed and not paid to anyone: no moderator ever\n// holds it, and there is no refund path.\n//\n// One pass per address per court, and it never expires. It only sets a floor —\n// standing earned later still lifts the poster above the entry rung.\n//\n// Refused when: the court does not exist; the caller already holds a pass here;\n// the court has not completed a sealed epoch, so no price can be quoted and its\n// board is not open at all; the court has no supply to take a fraction of; or\n// the caller's DisposableOf is short of the price.\nfunc BuyCommentPass(cur realm, courtSlug string) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\ttouch(c)\n\tif r := lookupStanding(c, who); r != nil \u0026\u0026 r.passHeld {\n\t\tpanic(\"kourtv2: this address already holds a pass in this court\")\n\t}\n\tprice := passPriceFor(c)\n\tif price \u003c= 0 {\n\t\t// Two ways to get here and the message must not pick the wrong one: a\n\t\t// court with no sealed epoch cannot quote a price at all, and a court\n\t\t// with zero sealed supply has nothing to take a fraction of.\n\t\tif _, sealed := boardSupply(c); !sealed {\n\t\t\tpanic(\"kourtv2: this court has not completed a sealed epoch yet; its board is not open\")\n\t\t}\n\t\tpanic(\"kourtv2: this court cannot price a pass yet — it has no supply\")\n\t}\n\tmustSpendable(c, who, price)\n\tc.coin.Burn(who, price)\n\tgetStanding(c, who).passHeld = true\n\treturn price\n}\n\n// HoldsPass reports whether an address has bought in.\nfunc HoldsPass(courtSlug string, who address) bool {\n\tr := lookupStanding(mustCourt(courtSlug), who)\n\treturn r != nil \u0026\u0026 r.passHeld\n}\n\n// ---- the throttle ---------------------------------------------------------\n\n// secsPerDay is the refill window. WALL-CLOCK, never height: clock.gno's rule is\n// that a deadline a user plans around gates on block time, because a height only\n// means a date if the chain's pace never changes — and \"eight posts a day\" is\n// exactly something a user plans around.\nconst secsPerDay = int64(86_400)\n\n// bucketAt is the WHOLE of the refill arithmetic, and it is one function because\n// the alternative was two.\n//\n// The write path (refill) and the read path (PostsAvailable) need the same\n// answer, and the first version of this file computed it twice — once in each.\n// Measured: three separate mutations of the write-path copy survived the suite,\n// because every assertion read the OTHER copy. That is the defect stakeIdxMine's\n// note names in one line — \"two copies of that check is how the second one comes\n// to disagree with the first\" — arrived at from the other direction, where the\n// copies agree and the tests are what stop meaning anything.\n//\n// Pure: no reads, no writes, no clock. Returns what is available now and the\n// stamp that goes with it.\n//\n// THE REMAINDER IS KEPT. The stamp advances by what the granted posts actually\n// cost, not to `now`, so a poster who checks every minute is not rounded down to\n// nothing forever. Advancing to now would make the effective rate a function of\n// how often anyone looked at the page.\n//\n// The bucket caps at ONE DAY of posts, so being away a week banks a day.\nfunc bucketAt(tokens, lastRefill, rate, now int64) (avail, stamp int64) {\n\tif rate \u003c= 0 {\n\t\treturn 0, lastRefill\n\t}\n\tif lastRefill == 0 { // first ever post: start full, not empty\n\t\treturn rate, now\n\t}\n\tstamp = lastRefill\n\tif gain := (now - lastRefill) * rate / secsPerDay; gain \u003e 0 {\n\t\ttokens = satAdd(tokens, gain)\n\t\tstamp += gain * secsPerDay / rate\n\t}\n\tif tokens \u003e rate {\n\t\ttokens = rate\n\t}\n\tif tokens \u003c 0 {\n\t\ttokens = 0\n\t}\n\treturn tokens, stamp\n}\n\n// refill brings a stored bucket up to date. The ONLY writer.\n//\n// It persists across everything — buying a pass, a slash, a freeze — and a\n// purchase never tops it up: the pass buys a rate, and granting a full bucket\n// with it would make buy-post-withdraw a way to spend one allowance twice.\nfunc refill(c *Court, who address, r *standingRow) int64 {\n\trate := postsPerDayAt(ladderFor(c), postLevel(c, who))\n\tr.tokens, r.lastRefill = bucketAt(r.tokens, r.lastRefill, rate, nowTime())\n\treturn r.tokens\n}\n\n// mustSpendPost takes one post from the bucket or refuses.\n//\n// The refusal names the LEVEL, because \"slow down\" is not actionable and \"you\n// are at level 1, which allows 3 a day\" tells a poster both what they have and\n// what earning more would buy.\nfunc mustSpendPost(c *Court, who address) {\n\tlvl := postLevel(c, who)\n\tif lvl == 0 {\n\t\t// The unsealed case is NOT \"earn standing or buy a pass\": neither is\n\t\t// available, and telling someone to buy something they cannot buy is the\n\t\t// same defect the frozen standing page had.\n\t\tif _, sealed := boardSupply(c); !sealed {\n\t\t\tpanic(\"kourtv2: this court has not completed a sealed epoch yet; its board is not open\")\n\t\t}\n\t\tpanic(\"kourtv2: you cannot post in this court yet — earn standing, or buy an entry pass\")\n\t}\n\tr := getStanding(c, who)\n\tif refill(c, who, r) \u003c 1 {\n\t\tpanic(\"kourtv2: your posting allowance for now is spent — level \" +\n\t\t\tstrconv.FormatInt(lvl, 10) + \" allows \" +\n\t\t\tstrconv.FormatInt(postsPerDayAt(ladderFor(c), lvl), 10) + \" a day\")\n\t}\n\tr.tokens--\n}\n\n// PostsAvailable is what an address may write right now. A READ, so it must not\n// move the bucket — it takes bucketAt's answer and discards the stamp rather\n// than reimplementing the arithmetic, which is how the two used to drift.\nfunc PostsAvailable(courtSlug string, who address) int64 {\n\tc := mustCourt(courtSlug)\n\tr := lookupStanding(c, who)\n\tif r == nil {\n\t\treturn 0\n\t}\n\tavail, _ := bucketAt(r.tokens, r.lastRefill, postsPerDayAt(ladderFor(c), postLevel(c, who)), nowTime())\n\treturn avail\n}\n\n// ------------------------------------------------------------------ render --\n\n// renderStanding is one address's page: what it has earned, what that buys, and\n// what a moderator has taken.\n//\n// CAPABILITY FACTS, and deliberately not a punishment dossier. MODERATION.md §2\n// declined to build an on-chain per-address index of a person's activity, and §6\n// forbids moderator copy that makes assertions of fact about a person. So this\n// page answers \"what may I do, and why\" — standing, level, rate, high-water mark\n// — and leaves the audit trail of individual acts to the moderation log, which is\n// addressed by row id and reachable by purge. A page headed with someone's\n// address listing what they were punished for is a claim about the person, in the\n// realm's own voice, with no §230 shield.\n//\n// The only user text here is an address, which still goes through the gate: a\n// bare string type carries no length invariant and IsValid() is not an escaping.\nfunc renderStanding(slug, addrArg string) string {\n\tc := mustCourt(slug)\n\twho := address(addrArg)\n\tif !who.IsValid() {\n\t\treturn \"## Not found\\nThat is not a valid address.\"\n\t}\n\tl := ladderFor(c)\n\tsup, sealed := boardSupply(c)\n\tlvl := postLevel(c, who)\n\n\tvar b strings.Builder\n\tb.WriteString(\"# Standing in \" + courtNameFor(c) + \"\\n\\n\")\n\tb.WriteString(\"`\" + sanitize.InlineText(who.String()) + \"`\\n\\n\")\n\tif !sealed {\n\t\t// The page renders rather than panicking, and says the true thing: this\n\t\t// court has no sealed epoch yet, so there is no threshold to be above or\n\t\t// below. It used to abort here with a grc20votes-internal message.\n\t\tb.WriteString(\"This court is too new to have a board. Its thresholds are quoted\\n\" +\n\t\t\t\"against a sealed epoch and it has not completed one yet; comments open\\n\" +\n\t\t\t\"as soon as it has.\\n\\n\")\n\t\tb.WriteString(helpLink + \"\\n\")\n\t\treturn b.String()\n\t}\n\n\tvar score, hw int64\n\tif r := lookupStanding(c, who); r != nil {\n\t\tscore, hw = r.score, r.highWater\n\t}\n\t// WHAT IS BLOCKING YOU, BEFORE WHAT YOU MAY DO. A frozen address used to\n\t// read this page as \"you cannot comment here YET — buy an entry pass\", which\n\t// invited a purchase that would not work, never mentioned the freeze, and\n\t// never said the pass they already held had been forfeited by the slash. The\n\t// freeze reads existed with no caller at all.\n\tfrozenAt, frozenUntil := boardFrozenAt(c, who)\n\tif frozenUntil != 0 {\n\t\t// The DATE first, the block as its reference. A freeze set before the\n\t\t// stamps has no date and shows the block alone, which is all it ever had.\n\t\twhen := \"block \" + strconv.FormatInt(frozenUntil, 10)\n\t\tif frozenAt != 0 {\n\t\t\twhen = strconv.FormatInt(frozenAt, 10) + \" (\" + when + \")\"\n\t\t}\n\t\tb.WriteString(\"\u003e **This court's moderators have frozen you out of its boards**\\n\" +\n\t\t\t\"\u003e until \" + when + \".\\n\u003e\\n\" +\n\t\t\t\"\u003e It reaches comments and upvotes and nothing else: you may still stake,\\n\" +\n\t\t\t\"\u003e vote, answer, dispute, flag, open claims, withdraw, draw emission, and\\n\" +\n\t\t\t\"\u003e take every election action. An entry pass will not lift it, and buying\\n\" +\n\t\t\t\"\u003e one now would burn coin for nothing.\\n\u003e\\n\" +\n\t\t\t\"\u003e The court's [moderation log](/r/kourt/kourtv2:\" + slug +\n\t\t\t\"/mod) records who acted and why.\\n\\n\")\n\t}\n\n\tb.WriteString(\"## What you may do\\n\\n\")\n\tb.WriteString(\"- level: **\" + strconv.FormatInt(lvl, 10) + \"** of 3\\n\")\n\tb.WriteString(\"- comments: **\" + strconv.FormatInt(postsPerDayAt(l, lvl), 10) +\n\t\t\" a day**, \" + strconv.FormatInt(PostsAvailable(slug, who), 10) + \" available now\\n\")\n\tb.WriteString(\"- per claim: at most \" + strconv.Itoa(maxBoardRowsPerAuthor) + \" comments\\n\")\n\t// TWO DIFFERENT FACTS WITH TWO DIFFERENT REMEDIES, and both are reported.\n\t// A freeze lifts on its own; a slash does not. Suppressing the slash line\n\t// while a freeze is live — which an earlier version did — means a thawed user\n\t// discovers their standing is gone and their pass with it, with nothing on\n\t// any page having said so.\n\tif lvl == 0 {\n\t\tif hw \u003e score {\n\t\t\tb.WriteString(\"\\nA moderator slash took your standing to zero, and with it the\\n\" +\n\t\t\t\t\"entry pass if you held one — the burn is not refunded. A successor\\n\" +\n\t\t\t\t\"set, or the platform, can restore up to the high-water mark below.\\n\")\n\t\t}\n\t\t// The INVITATION is what a freeze suppresses: a pass floors the level at\n\t\t// 1 and would otherwise work, so offering it to a frozen address invites\n\t\t// a purchase that cannot help until the freeze lifts.\n\t\tif frozenUntil == 0 {\n\t\t\tif hw \u003e score {\n\t\t\t\tb.WriteString(\"You can also earn back from zero, or buy a new pass for \" +\n\t\t\t\t\tstrconv.FormatInt(passPriceFor(c), 10) + \" \" + qualifiedSymbol(c) + \".\\n\")\n\t\t\t} else {\n\t\t\t\tb.WriteString(\"\\nYou cannot comment here yet. Earn standing below, or buy an entry\\n\" +\n\t\t\t\t\t\"pass for \" + strconv.FormatInt(passPriceFor(c), 10) + \" \" +\n\t\t\t\t\tqualifiedSymbol(c) + \" (burned, one-off).\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\tb.WriteString(\"\\n## Standing\\n\\n\")\n\tb.WriteString(\"- earned: **\" + strconv.FormatInt(score, 10) + \"**\\n\")\n\tb.WriteString(\"- high-water mark: \" + strconv.FormatInt(hw, 10) +\n\t\t\" — what a moderator slash can be restored toward\\n\")\n\tb.WriteString(\"- entry pass: \" + yesNo(HoldsPass(slug, who)) + \"\\n\")\n\t// WHERE IT CAME FROM, IN WORDS, AND ONLY WHAT IS THERE. This line used to\n\t// print StandingBreakdown verbatim — a semicolon-separated WIRE format, on a\n\t// human page, listing four categories most of which are zero for most\n\t// addresses. StandingBreakdown stays exactly as it is: it is the qeval read\n\t// it was written to be, and a client parsing it is the reason it exists.\n\twriteStandingFrom(\u0026b, c, who)\n\n\tb.WriteString(\"\\n## The rungs, in this court\\n\\n\")\n\tfor _, k := range []struct {\n\t\tname string\n\t\tbps  int64\n\t\trate int64\n\t}{{\"1\", l.t1Bps, l.postsEntry}, {\"2\", l.t2Bps, l.postsT2}, {\"3\", l.t3Bps, l.postsT3()}} {\n\t\tb.WriteString(\"- level \" + k.name + \": \" + strconv.FormatInt(sup*k.bps/10_000, 10) +\n\t\t\t\" standing (\" + strconv.FormatInt(k.bps, 10) + \" bps of supply) → \" +\n\t\t\tstrconv.FormatInt(k.rate, 10) + \" a day\\n\")\n\t}\n\n\t// THE CONSEQUENCE OF A BPS BAR, said once. The rungs above are a share of a\n\t// sealed supply that only grows, so a score that never moves still crosses\n\t// back down a rung on its own. §2.3's \"standing never decays\" is true of the\n\t// NUMBER and says nothing about the rung, which is what actually gates\n\t// posting — and a reader who lost a rung with no notice has no other way to\n\t// learn why.\n\tb.WriteString(\"\\nEach rung is a **share of this court's supply**, not a fixed \" +\n\t\t\"number, so the bar rises as the court grows. Standing itself is never \" +\n\t\t\"taken except by a logged moderator slash — but a score that does not \" +\n\t\t\"move can still fall below a rung it used to clear.\\n\")\n\n\tb.WriteString(\"\\n## How standing is earned\\n\\n\")\n\tb.WriteString(\"Only where the court adjudicated something, and always in proportion\\n\" +\n\t\t\"to your own capital that the act committed or destroyed:\\n\\n\")\n\tb.WriteString(\"- a quality flag whose slash **settled** — the full bar, two thirds low\\n\")\n\tb.WriteString(\"- **prevailing in a dispute** — an uphold against real opposition, or an overturn\\n\")\n\tb.WriteString(\"- authoring a claim the court rated **HIGH** — seeded claims earn nothing\\n\")\n\tb.WriteString(\"- **winning conviction** on a resolved claim; half where nobody disputed it\\n\\n\")\n\tb.WriteString(\"Never earned: commenting, being upvoted, buying \" +\n\t\tqualifiedSymbol(c) + \", or buying the pass.\\n\")\n\tb.WriteString(\"Standing does not decay. A moderator may slash it for abuse; the\\n\")\n\tb.WriteString(\"high-water mark above is what a successor set can restore.\\n\")\n\n\tb.WriteString(\"\\n## The answer record\\n\\n\")\n\tb.WriteString(\"- contested-and-upheld answers: **\" +\n\t\tstrconv.FormatInt(int64(AnswerRecord(slug, who)), 10) + \"**\\n\")\n\tb.WriteString(\"\\n_Adjudicated — moderators cannot change this._\\n\")\n\treturn b.String()\n}\n\n// renderStandingIndex is /\u003cslug\u003e/me with no address: the rules, and nothing\n// about anybody.\nfunc renderStandingIndex(slug string) string {\n\tc := mustCourt(slug)\n\tl := ladderFor(c)\n\tvar b strings.Builder\n\tb.WriteString(\"# Standing in \" + courtNameFor(c) + \"\\n\\n\")\n\tb.WriteString(\"To see an address's standing, put it on the end of this \" +\n\t\t\"page's path: `/r/kourt/kourtv2:\" + sanitize.InlineText(slug) +\n\t\t\"/me/\u003caddress\u003e`\\n\\n\")\n\tb.WriteString(\"An entry pass costs \" + strconv.FormatInt(passPriceFor(c), 10) +\n\t\t\" \" + qualifiedSymbol(c) + \", burned, once. Standing is earned and \" +\n\t\t\"cannot be bought.\\n\\n\")\n\t// bps IS THE REALM'S WORD, not the reader's. The three thresholds below are\n\t// meaningless without it, and a term defined nowhere on the page it is used\n\t// on is a term the reader is expected to already know.\n\tb.WriteString(\"Standing is measured against this court's coin supply, in \" +\n\t\t\"basis points — one bp is a hundredth of a percent.\\n\\n\")\n\tb.WriteString(\"- level 1 at \" + strconv.FormatInt(l.t1Bps, 10) + \" bps of supply → \" +\n\t\tstrconv.FormatInt(l.postsEntry, 10) + \" comments a day\\n\")\n\tb.WriteString(\"- level 2 at \" + strconv.FormatInt(l.t2Bps, 10) + \" bps → \" +\n\t\tstrconv.FormatInt(l.postsT2, 10) + \" a day\\n\")\n\tb.WriteString(\"- level 3 at \" + strconv.FormatInt(l.t3Bps, 10) + \" bps → \" +\n\t\tstrconv.FormatInt(l.postsT3(), 10) + \" a day\\n\")\n\treturn b.String()\n}\n\n// writeStandingFrom names the categories an address actually earned in, and says\n// nothing at all when there are none — \"flag: 0, dispute: 0, author: 0,\n// conviction: 0\" tells a new participant four things that are not true of them.\nfunc writeStandingFrom(b *strings.Builder, c *Court, who address) {\n\tr := lookupStanding(c, who)\n\tif r == nil {\n\t\treturn\n\t}\n\tlabels := [standingCatN]string{\n\t\t\"quality flags whose slash settled\",\n\t\t\"disputes you prevailed in\",\n\t\t\"claims of yours the court rated HIGH\",\n\t\t\"conviction on resolved claims\",\n\t}\n\tlisted := false\n\tfor i := 0; i \u003c standingCatN; i++ {\n\t\tif r.from[i] == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif !listed {\n\t\t\tb.WriteString(\"- from: \")\n\t\t\tlisted = true\n\t\t} else {\n\t\t\tb.WriteString(\"; \")\n\t\t}\n\t\tb.WriteString(strconv.FormatInt(r.from[i], 10) + \" from \" + labels[i])\n\t}\n\tif listed {\n\t\tb.WriteString(\"\\n\")\n\t}\n}\n\nfunc yesNo(b bool) string {\n\tif b {\n\t\treturn \"held\"\n\t}\n\treturn \"none\"\n}\n"},{"name":"records.gno","body":"// Answer records: the non-transferable, difficulty-weighted credential (PLAN\n// §3.8, pin P13). Only a CONTESTED-AND-UPHELD answer earns a point — an\n// undisputed stand earns nothing (raw counting would reward cherry-picking\n// trivially-true claims, the mill's habitat — A18); an overturn RESETS the\n// record to zero (lying spends the whole credential). A record of\n// priorityNetRecord+ earns the 24h priority window on the answer slot, gated\n// off court-wide until priorityColdStartN addresses qualify (no launch-wide\n// latency tax), and limited to ONE active priority claim per address at a time\n// (no flywheel blanketing).\n\npackage kourtv2\n\n// answerRecord is one address's standing in one court.\ntype answerRecord struct {\n\tscore          int    // contested-and-upheld count; reset to 0 on overturn\n\tactivePriority uint64 // claim currently held via the priority window (0 = none)\n}\n\n// creditUpheld is called by the dispute machine when a contested answer is\n// UPHELD by a decided vote (never on undisputed settles — P13).\nfunc creditUpheld(c *Court, who address) {\n\tr := getRecord(c, who)\n\twasQualified := r.score \u003e= priorityNetRecord\n\tr.score++\n\tif !wasQualified \u0026\u0026 r.score \u003e= priorityNetRecord {\n\t\tc.qualifiedCount++\n\t}\n}\n\n// resetOverturned is called when an answer is overturned: the credential burns\n// entirely — the record prices a CAREER of honesty, the bond priced the lie.\nfunc resetOverturned(c *Court, who address) {\n\tr := getRecord(c, who)\n\tif r.score \u003e= priorityNetRecord {\n\t\tc.qualifiedCount--\n\t}\n\tr.score = 0\n}\n\n// priorityGateActive: the 24h window binds only once the court has real\n// qualified answerers (cold-start guard, R5e).\nfunc priorityGateActive(c *Court) bool { return c.qualifiedCount \u003e= priorityColdStartN }\n\n// mayAnswerInPriority: qualified, and not already holding a priority claim.\nfunc mayAnswerInPriority(c *Court, who address) bool {\n\tr := getRecord(c, who)\n\treturn r.score \u003e= priorityNetRecord \u0026\u0026 r.activePriority == 0\n}\n\n// holdPriority / releasePriority bracket the one-active rule (P13): held from\n// the priority answer until that claim finalizes.\nfunc holdPriority(c *Court, who address, claimID uint64) {\n\tgetRecord(c, who).activePriority = claimID\n}\n\nfunc releasePriority(c *Court, who address, claimID uint64) {\n\tr := getRecord(c, who)\n\tif r.activePriority == claimID {\n\t\tr.activePriority = 0\n\t}\n}\n\nfunc getRecord(c *Court, who address) *answerRecord {\n\tif v := c.records.Get(string(who)); v != nil {\n\t\treturn v.(*answerRecord)\n\t}\n\tr := \u0026answerRecord{}\n\tc.records.Set(string(who), r)\n\treturn r\n}\n\n// ------------------------------------------------------------------ reads --\n\n// AnswerRecord is an address's difficulty-weighted score in a court.\nfunc AnswerRecord(courtSlug string, who address) int {\n\tc := mustCourt(courtSlug)\n\tif v := c.records.Get(string(who)); v != nil {\n\t\treturn v.(*answerRecord).score\n\t}\n\treturn 0\n}\n\n// PriorityGateActive reports whether the 24h priority window currently binds.\nfunc PriorityGateActive(courtSlug string) bool {\n\treturn priorityGateActive(mustCourt(courtSlug))\n}\n"},{"name":"relrender.gno","body":"// A claim's relations to other claims, which rendered nowhere.\n//\n// THE FOURTH INSTANCE OF ONE PATTERN, after the comment board (a working route\n// nothing linked to), the filing tree (seven verbs, no page) and the ballot\n// (fourteen entrypoints, no page). association.gno and supersede.gno export\n// about fifteen entrypoints between them — add, remove, approve, disapprove,\n// claim the bond, the batch verbs, and the two packed reads — and no\n// human-facing page drew a single edge. The overlay renders them; gnoweb did\n// not, so the two surfaces disagreed about what a claim even is.\n//\n// AND SUPERSEDES IS THE ONE THAT COSTS SOMETHING. An argument edge is a view\n// somebody holds. A supersede edge is a statement about the docket's own\n// history, verified by the chain: it says this claim's question died unanswered\n// and was re-filed as another. A reader sitting on a superseded claim, with coin\n// staked on it, had no way to learn that the live version of their question is\n// somewhere else. That is not a missing feature, it is a reader left on a page\n// the docket has moved past.\n\npackage kourtv2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n// writeRelations draws the supersede edges and the argument edges for one claim.\n//\n// Both halves are gated the way the packed reads gate them — assocTextGone\n// keeps a purged or globally-redacted claim's id off another claim's page — so\n// this renders exactly the set the qeval reads would return, and no more. A\n// render that showed an edge the read withholds would put a withheld claim back\n// in front of a reader by a side door.\nfunc writeRelations(b *strings.Builder, c *Court, claimID uint64) {\n\tbase := \"/r/kourt/kourtv2:\" + c.id + \"/\"\n\tsup := writeSupersedes(b, c, claimID, base)\n\targ := writeArguments(b, c, claimID, base)\n\tif sup || arg {\n\t\tb.WriteString(\"\\n\")\n\t}\n}\n\n// writeSupersedes is \"this claim re-files that one\" and \"these re-file this\n// one\", in that order.\n//\n// THE INCOMING EDGE IS THE URGENT ONE and it goes FIRST, before the claim's own\n// signal, because it changes what the page below it is worth reading. Everything\n// else about relations is context; this is a redirection.\nfunc writeSupersedes(b *strings.Builder, c *Court, claimID uint64, base string) bool {\n\twrote := false\n\tif c.supBy != nil {\n\t\tp := beClaimKey(claimID)\n\t\tn := 0\n\t\tc.supBy.Iterate(p, \"\", func(key string, v any) bool {\n\t\t\tif !strings.HasPrefix(key, p) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tfrom := claimIDFromKey(key[len(p):])\n\t\t\tif from == 0 || from \u003e c.nextID || assocTextGone(c, from) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif n == 0 {\n\t\t\t\tb.WriteString(\"\u003e **This question was asked again.** It died here \" +\n\t\t\t\t\t\"without an answer and was re-filed as:\\n\")\n\t\t\t}\n\t\t\tn++\n\t\t\tcs := mustClaim(c, from)\n\t\t\tb.WriteString(\"\u003e\\n\u003e - [\" + claimTitleFor(c, cs) + \"](\" + base +\n\t\t\t\tstrconv.FormatUint(from, 10) + \") — \" + claimStatus(cs) + \"\\n\")\n\t\t\treturn n \u003e= renderPageSize\n\t\t})\n\t\tif n \u003e 0 {\n\t\t\tb.WriteString(\"\\n\")\n\t\t\twrote = true\n\t\t}\n\t}\n\tif c.supOf != nil {\n\t\tif v := c.supOf.Get(beClaimKey(claimID)); v != nil {\n\t\t\te := v.(*supEdge)\n\t\t\tif e.to != 0 \u0026\u0026 e.to \u003c= c.nextID \u0026\u0026 !assocTextGone(c, e.to) {\n\t\t\t\tcs := mustClaim(c, e.to)\n\t\t\t\tb.WriteString(\"_Re-files [\" + claimTitleFor(c, cs) + \"](\" + base +\n\t\t\t\t\tstrconv.FormatUint(e.to, 10) + \"), which died unanswered._\\n\\n\")\n\t\t\t\twrote = true\n\t\t\t}\n\t\t}\n\t}\n\treturn wrote\n}\n\n// writeArguments is the support/contest graph.\n//\n// NAMED FOR WHAT IT IS, not for the field. \"association\" is the code's word and\n// says nothing; the page says a claim supports or contests another. And the\n// direction is spelled out both ways, because \"out\" and \"in\" are meaningless to\n// a reader: an edge this claim asserts is not the same fact as an edge somebody\n// else pointed at it, and conflating them would let anyone put words in a\n// claim's mouth.\n//\n// THE PAGE SAYS THESE ARE OPINIONS. Anyone may add one and the chain checks\n// nothing — association.gno's own header says an argument edge is speech — so a\n// reader who took this list for an adjudicated finding would be reading a\n// stranger's claim as the court's.\nfunc writeArguments(b *strings.Builder, c *Court, claimID uint64, base string) bool {\n\tvar out, in strings.Builder\n\tnOut := writeEdgeSide(\u0026out, c, claimID, base, true)\n\tnIn := writeEdgeSide(\u0026in, c, claimID, base, false)\n\tif nOut+nIn == 0 {\n\t\treturn false\n\t}\n\tb.WriteString(\"## Related claims\\n\\n\")\n\tb.WriteString(\"_Anyone may draw these links, and the court does not rule on \" +\n\t\t\"them — they are what readers have asserted, not findings._\\n\\n\")\n\tif nOut \u003e 0 {\n\t\tb.WriteString(\"This claim says:\\n\\n\")\n\t\tb.WriteString(out.String() + \"\\n\")\n\t}\n\tif nIn \u003e 0 {\n\t\tb.WriteString(\"Others say about this claim:\\n\\n\")\n\t\tb.WriteString(in.String() + \"\\n\")\n\t}\n\treturn true\n}\n\n// writeEdgeSide walks one direction of the argument graph.\nfunc writeEdgeSide(b *strings.Builder, c *Court, claimID uint64, base string, outgoing bool) int {\n\tt := c.assocIn\n\tif outgoing {\n\t\tt = c.assocOut\n\t}\n\tif t == nil {\n\t\treturn 0\n\t}\n\tp := beClaimKey(claimID)\n\tn := 0\n\tt.Iterate(p, \"\", func(key string, v any) bool {\n\t\tif !strings.HasPrefix(key, p) {\n\t\t\treturn true\n\t\t}\n\t\te, ok := v.(*assocEdge)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\tother := claimIDFromKey(key[len(p):])\n\t\tif other == 0 || other \u003e c.nextID || assocTextGone(c, other) {\n\t\t\treturn false\n\t\t}\n\t\tverb := \"supports\"\n\t\tif e.stance == assocContests {\n\t\t\tverb = \"contests\"\n\t\t}\n\t\tif !outgoing {\n\t\t\tverb += \" it\"\n\t\t}\n\t\tn++\n\t\tcs := mustClaim(c, other)\n\t\tb.WriteString(\"- \" + verb + \" [\" + claimTitleFor(c, cs) + \"](\" + base +\n\t\t\tstrconv.FormatUint(other, 10) + \")\\n\")\n\t\treturn n \u003e= renderPageSize\n\t})\n\treturn n\n}\n"},{"name":"render.gno","body":"// Render: the claim page IS the product (PLAN §3.9/§11). Three ratio series,\n// no \"backing\" anywhere (burned GNOT backs nothing), sealed tallies once votes\n// exist, absolute CC figures only — never percentages of return, never the\n// inflation ceiling (§7.4). Every user string passes the sanitizer.\n\npackage kourtv2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\tsanitize \"gno.land/p/nt/markdown/sanitize/v0\"\n)\n\n// Render routes:\n//\n//\t\"\"                    the directory\n//\t\"\u003cslug\u003e\"              one court\n//\t\"\u003cslug\u003e/\u003cid\u003e\"         one claim\n//\t\"\u003cslug\u003e/folder/\u003cfid\u003e\" one folder\n//\t\"\u003cslug\u003e/election\"     the moderator ballot\n//\t\"\u003cslug\u003e/\u003cid\u003e/\u003caddr\u003e\"  one address's positions on a claim (\"your page\")\nfunc Render(path string) string {\n\t// An armed test clock fabricates every date below. Say so on EVERY page,\n\t// first thing: an accessor nobody reads is not a disclosure, and gnoweb\n\t// serves this output directly to readers who have no other signal.\n\t// The overlay header sits beside the clock banner and for the same reason: a\n\t// disclosure that appears on some pages and not others is one a reader cannot\n\t// rely on. Empty when no domain is configured (sitelink.gno), so a realm with\n\t// no website renders exactly as it always did.\n\treturn withSiteBanner(render(path), path)\n}\n\nfunc render(path string) string {\n\tpath = strings.Trim(path, \"/\")\n\tif path == \"\" {\n\t\treturn renderDirectory()\n\t}\n\t// FOUR, not three. The cap used to be the thing that refused a fourth\n\t// segment — it folded into the address argument and IsValid() rejected it —\n\t// and the board's per-row deep link needs a fourth, so the refusal moved to\n\t// an explicit arm below. `\u003cslug\u003e/\u003cid\u003e/\u003caddr\u003e/extra` still renders \"Not\n\t// found\"; it is a different line that says so.\n\tparts := strings.SplitN(path, \"/\", 4)\n\tif len(parts) == 1 {\n\t\t// The help page, carved before the court arm. It cannot shadow a court:\n\t\t// a slug is at most 11 characters and this path is 12, so the two sets\n\t\t// are disjoint by construction rather than by a reserved word somebody\n\t\t// has to remember to keep reserved (help.gno).\n\t\tif parts[0] == helpPath {\n\t\t\treturn renderHelp()\n\t\t}\n\t\t// Carved here for exactly the reason helpPath is, and safe by the same\n\t\t// construction: this path is 12 characters and a slug is at most 11, so\n\t\t// no court can ever shadow it.\n\t\tif parts[0] == adminParamsPath {\n\t\t\treturn renderAdminParams()\n\t\t}\n\t\treturn renderCourt(parts[0])\n\t}\n\t// The moderation log is a named sub-route, so it must be carved out BEFORE\n\t// the numeric-id parse below (MODERATION.md §4).\n\tif parts[1] == \"mod\" {\n\t\treturn renderModLog(parts[0])\n\t}\n\t// The standing page carves at the same position and for the same reason. A\n\t// slug is [a-z0-9-]{1,11} so a court MAY be called \"me\", but that occupies\n\t// parts[0]; and a claim id is numeric, so \"me\" can never be mistaken for one.\n\t// FOLDERS, which rendered NOWHERE. A court's moderators can build a whole\n\t// filing tree — seven verbs, an events filetest, its own mutation rows — and\n\t// no page in this realm ever drew one. Same defect the board had: a feature\n\t// that exists, is written to, and is unreachable by anybody who is not\n\t// running the overlay. Carved here for the reason \"mod\" and \"me\" are carved\n\t// here: a folder id is numeric, so the word can never be taken for a claim.\n\t// THE BALLOT, which rendered nowhere at all — fourteen entrypoints and no\n\t// page (docs §21). Carved beside \"mod\" and \"me\" for the same reason: a claim\n\t// id is numeric, so a word can never be taken for one.\n\tif parts[1] == electionPath {\n\t\tif len(parts) != 2 {\n\t\t\treturn \"## Not found\\nThe ballot is /\u003ccourt\u003e/election.\"\n\t\t}\n\t\treturn renderElection(parts[0])\n\t}\n\tif parts[1] == \"folder\" {\n\t\tif len(parts) != 3 {\n\t\t\treturn \"## Not found\\nA folder is /\u003ccourt\u003e/folder/\u003cid\u003e.\"\n\t\t}\n\t\tfid, ferr := strconv.ParseUint(parts[2], 10, 64)\n\t\tif ferr != nil {\n\t\t\treturn \"## Not found\\nA folder id must be a number.\"\n\t\t}\n\t\treturn renderFolderPage(parts[0], fid)\n\t}\n\tif parts[1] == \"me\" {\n\t\tif len(parts) == 3 {\n\t\t\treturn renderStanding(parts[0], parts[2])\n\t\t}\n\t\treturn renderStandingIndex(parts[0])\n\t}\n\tid, err := strconv.ParseUint(parts[1], 10, 64)\n\tif err != nil {\n\t\treturn \"## Not found\\nA claim id must be a number.\"\n\t}\n\t// The board: a page at three segments, one row at four. Carved BEFORE the\n\t// positions arm, and deliberately without touching that arm's text — two\n\t// mutation rows anchor on it verbatim, and an edit there would leave them\n\t// measuring nothing.\n\tif len(parts) \u003e= 3 \u0026\u0026 parts[2] == \"board\" {\n\t\tif len(parts) == 4 {\n\t\t\t// \"top\" is the ranked VIEW of the same board, carved before the\n\t\t\t// numeric parse for the reason \"mod\" and \"me\" are: a row id is\n\t\t\t// numeric, so a word can never be mistaken for one.\n\t\t\tif parts[3] == \"top\" {\n\t\t\t\treturn renderBoardTop(parts[0], id)\n\t\t\t}\n\t\t\t// \"withheld\" carves here for the same reason \"top\" does: a row id\n\t\t\t// is numeric, so a word can never be mistaken for one.\n\t\t\tif parts[3] == \"hidden\" {\n\t\t\t\treturn renderBoardHidden(parts[0], id)\n\t\t\t}\n\t\t\treturn renderBoardRow(parts[0], id, parts[3])\n\t\t}\n\t\treturn renderBoardPage(parts[0], id)\n\t}\n\tif len(parts) == 4 {\n\t\treturn \"## Not found\\nThat is not a page.\"\n\t}\n\tif len(parts) == 3 {\n\t\treturn renderPositions(parts[0], id, parts[2])\n\t}\n\treturn renderClaim(parts[0], id)\n}\n\nfunc renderDirectory() string {\n\tb := strings.Builder{}\n\tb.WriteString(\"# \" + platformName + \"\\n\\nLet Truth be told.\\n\\n\")\n\t// THE WAY IN, on the page every stranger lands on first. Read cold, the\n\t// motto above says nothing about what happens here, and a list of court\n\t// names says less. The explanation existed only in docs/ and in the overlay,\n\t// neither of which a gnoweb reader can reach from here.\n\t//\n\t// AND THE PROMISE STAYS ON THE PAGE. The motto replaced a lede that carried\n\t// the one fact that distinguishes this from a betting market — a stake is\n\t// returned whole, win or lose. render_test asserts it is here, which is how\n\t// its removal was caught; the overlay says it twice more, but a gnoweb reader\n\t// has only this page.\n\tb.WriteString(\"Stake on claims of fact. Your principal always returns 1×.\\n\\n\")\n\tb.WriteString(helpLink + \"\\n\\n\")\n\tafterHeader := b.Len()\n\twriteTier(\u0026b, \"Featured\", tierFeatured)\n\t// The listed tier ranks by cumulative GNOT burned into each court — the one\n\t// signal nobody can fake, so the front page needs no gameable activity or\n\t// vote score (MODERATION.md §3.2).\n\twriteListedByBurn(\u0026b)\n\tif b.Len() == afterHeader {\n\t\tb.WriteString(\"_No courts yet._\\n\")\n\t}\n\treturn b.String()\n}\n\nfunc writeListedByBurn(b *strings.Builder) {\n\tslugs := listedPage(\"burn\", 0, renderPageSize)\n\tif len(slugs) == 0 {\n\t\treturn\n\t}\n\tb.WriteString(\"## Courts\\n\\n\")\n\tshown := 0\n\tfor _, s := range slugs {\n\t\tc := mustCourt(s)\n\t\tif c.tier != tierListed {\n\t\t\tcontinue\n\t\t}\n\t\tshown++\n\t\tb.WriteString(\"- [\" + courtNameFor(c) + \"](/r/kourt/kourtv2:\" + s + \") · \" +\n\t\t\tqualifiedSymbol(c) + \" — \" + strconv.FormatUint(c.nextID, 10) + \" claims\\n\")\n\t}\n\t// A CAPPED LIST MUST SAY WHAT IT DROPPED. writeTier has always announced its\n\t// omissions; when the listed tier moved to burn-ranking it stopped, and a\n\t// silently truncated directory reads as the whole directory — a court past\n\t// row 50 looks like it does not exist. Counted against the live tier rather\n\t// than the page, because the page is what was capped.\n\tif total := len(ListByTier(int(tierListed))); total \u003e shown {\n\t\tb.WriteString(\"- _…and \" + strconv.Itoa(total-shown) +\n\t\t\t\" more; open any court directly at /r/kourt/kourtv2:\u003cslug\u003e_\\n\")\n\t}\n\tb.WriteString(\"\\n\")\n}\n\n// renderPageSize bounds every render loop to a constant so a page's gas cost\n// cannot grow with the (attacker-openable, deposit-priced) claim or court\n// count (audit N3). Deep links (…/\u003cslug\u003e, …/\u003cslug\u003e/\u003cid\u003e) reach anything the\n// capped list omits.\nconst renderPageSize = 50\n\nfunc writeTier(b *strings.Builder, heading string, tier uint8) {\n\tslugs := ListByTier(int(tier))\n\tif len(slugs) == 0 {\n\t\treturn\n\t}\n\tb.WriteString(\"## \" + heading + \"\\n\\n\")\n\tshown := len(slugs)\n\tif shown \u003e renderPageSize {\n\t\tshown = renderPageSize\n\t}\n\tfor _, s := range slugs[:shown] {\n\t\tc := mustCourt(s)\n\t\tb.WriteString(\"- [\" + courtNameFor(c) + \"](/r/kourt/kourtv2:\" + s + \") · \" +\n\t\t\tqualifiedSymbol(c) + \" — \" + strconv.FormatUint(c.nextID, 10) + \" claims\\n\")\n\t}\n\tif len(slugs) \u003e shown {\n\t\tb.WriteString(\"- _…and \" + strconv.Itoa(len(slugs)-shown) +\n\t\t\t\" more; open any court directly at /r/kourt/kourtv2:\u003cslug\u003e_\\n\")\n\t}\n\tb.WriteString(\"\\n\")\n}\n\nfunc renderCourt(slug string) string {\n\tif !Exists(slug) {\n\t\treturn \"## Not found\\nNo court by that slug.\"\n\t}\n\tc := mustCourt(slug)\n\tb := strings.Builder{}\n\tb.WriteString(\"# \" + courtNameFor(c) + \"\\n\\n\")\n\t// DELISTED, SAID OUT LOUD. A global DAO member may set a court to\n\t// tierHidden, which removes it from the directory and from every listing\n\t// read — and no page said so. A hidden CLAIM gets one of four banners naming\n\t// the authority that hid it; a hidden COURT got silence, so its creator and\n\t// its holders watched it disappear from the front page with nothing anywhere\n\t// to tell them it had been an act rather than a bug.\n\t//\n\t// The wording follows the claim's meta banner, because the situation is the\n\t// same one: listed nowhere, reachable here, lifecycle untouched. That last\n\t// part matters most — a delisted court still settles its claims and pays out.\n\tif c.tier == tierHidden {\n\t\tb.WriteString(\"\u003e **Delisted by the global DAO.** This court appears in no \" +\n\t\t\t\"listing and is reachable only by direct link. Nothing else changes: \" +\n\t\t\t\"its claims still stake, answer, settle and pay out as usual.\\n\\n\")\n\t}\n\tif d := courtDescFor(c); d != \"\" {\n\t\tb.WriteString(d + \"\\n\\n\")\n\t}\n\t// The filing tree, as ONE LINE of links rather than a section. It is\n\t// navigation — a way INTO the claims — so it belongs above them; and it is\n\t// one line so that putting it there does not re-create the thing the coin\n\t// block was moved out of the way for.\n\twriteFolderIndex(\u0026b, c, slug)\n\t// THE CLAIMS FIRST. Four lines of coin plumbing used to sit here, between a\n\t// reader and the only thing on this page they arrived for. Read cold,\n\t// \"supply: 5108592379 · emitted: 0\" and \"reward reservoir: 0 · senior queue\n\t// owed: 0\" are not an introduction to a court — they are the accounting of\n\t// one, which is a thing you want AFTER you have decided to care. It is all\n\t// still here, below the claims, where writeCourtCoin puts it.\n\tb.WriteString(\"## Claims\\n\\n\")\n\tif c.nextID == 0 {\n\t\tb.WriteString(\"_No claims yet._\\n\\n\")\n\t\t// Still shown on an empty court: the coin exists from the moment the\n\t\t// court does, and its price is exactly what somebody deciding whether\n\t\t// to be early wants to see.\n\t\twriteCourtCoin(\u0026b, c)\n\t\treturn b.String()\n\t}\n\t// Newest first, bounded to one page (audit N3): a court with thousands of\n\t// deposit-priced claims must not make its page cost unbounded. Older claims\n\t// stay reachable by direct id link.\n\tlo := uint64(1)\n\tif c.nextID \u003e renderPageSize {\n\t\tlo = c.nextID - renderPageSize + 1\n\t}\n\tfor id := c.nextID; id \u003e= lo; id-- {\n\t\tcs := mustClaim(c, id)\n\t\t// The ONE place a hide bit subtracts: ordinary discovery listings. The\n\t\t// claim stays reachable by deep link, and if it is still policeable it\n\t\t// stays on the \"needs review\" strip below (§5.3).\n\t\tif HiddenFromListing(slug, id) {\n\t\t\tcontinue\n\t\t}\n\t\tb.WriteString(\"- [\" + claimTitleFor(c, cs) + \"](/r/kourt/kourtv2:\" + slug + \"/\" +\n\t\t\tstrconv.FormatUint(id, 10) + \") — \" + claimStatus(cs) + \"\\n\")\n\t}\n\tif lo \u003e 1 {\n\t\tb.WriteString(\"- _…and \" + strconv.FormatUint(lo-1, 10) +\n\t\t\t\" older; open any by id at /r/kourt/kourtv2:\" + slug + \"/\u003cid\u003e_\\n\")\n\t}\n\tb.WriteString(\"\\n\")\n\twriteStrip(\u0026b, slug)\n\twritePending(\u0026b, slug)\n\twriteCourtCoin(\u0026b, c)\n\tb.WriteString(\"[Moderation log](/r/kourt/kourtv2:\" + slug + \"/mod)\\n\")\n\treturn b.String()\n}\n\n// writeCourtCoin is the court's accounting, below the claims rather than above\n// them.\n//\n// EVERY LINE CARRIES ITS OWN GLOSS. These are the realm's four house terms —\n// supply, emitted, reservoir, senior queue — and each was printed as a bare\n// label beside a ten-digit number. A number whose name you do not know is worse\n// than no number: it reads as something you are failing to understand rather\n// than something nobody told you. The gloss is short and in the same sentence,\n// because a legend somewhere else is a legend nobody reads.\nfunc writeCourtCoin(b *strings.Builder, c *Court) {\n\tb.WriteString(\"## This court's coin\\n\\n\")\n\tb.WriteString(\"- \" + qualifiedSymbol(c) +\n\t\t\" — this court's own coin, not interchangeable with any other court's\\n\")\n\tb.WriteString(\"- price: \" + strconv.FormatInt(c.crv.Price(c.minted), 10) +\n\t\t\" ugnot per coin — buying burns the GNOT, there is no sell side, \" +\n\t\t\"and the price only rises\\n\")\n\tb.WriteString(\"- in circulation: \" + strconv.FormatInt(c.coin.TotalSupply(), 10) +\n\t\t\" · paid out as rewards so far: \" + strconv.FormatInt(c.emittedTotal, 10) + \"\\n\")\n\tb.WriteString(\"- reward reservoir: \" + strconv.FormatInt(c.reservoirR(), 10) +\n\t\t\" waiting to be earned · \" + strconv.FormatInt(c.seniorOwed, 10) +\n\t\t\" already owed to earlier earners\\n\\n\")\n\tb.WriteString(helpLink + \"\\n\\n\")\n}\n\nfunc renderClaim(slug string, id uint64) string {\n\tif !Exists(slug) {\n\t\treturn \"## Not found\\nNo court by that slug.\"\n\t}\n\tc := mustCourt(slug)\n\tif id == 0 || id \u003e c.nextID {\n\t\treturn \"## Not found\\nNo claim by that id.\"\n\t}\n\tcs := mustClaim(c, id)\n\tb := strings.Builder{}\n\t// A deep link ALWAYS renders — hiding removes a claim from lists, never from\n\t// its own page — with a banner naming the authority. Text itself is withheld\n\t// only by the global (legal-hold) bit or a purge, and even then IDs, state,\n\t// deadlines and balances still render so a participant can always act.\n\tb.WriteString(\"# \" + claimTitleFor(c, cs) + \"\\n\\n\")\n\t// The author's own statement of what the claim means, above the money. It is\n\t// gated by claimBodyFor, so a purged or globally-redacted claim prints nothing\n\t// here — and it goes ABOVE the banner because a reader deciding whether to\n\t// stake should read the question before the state of the market on it.\n\tif body := claimBodyQuoted(c, cs); body != \"\" {\n\t\tb.WriteString(body)\n\t}\n\t// The evidence goes with the statement, above the money, for the body's\n\t// reason: a reader deciding whether to stake should see what the claim rests\n\t// on before the state of the market on it. Gated by claimMediaVisible, so a\n\t// purged or globally-redacted claim shows nothing here either.\n\twriteClaimMedia(\u0026b, c, cs)\n\tb.WriteString(hideBanner(c, cs))\n\twriteRelations(\u0026b, c, id)\n\twriteAppeal(\u0026b, c, cs)\n\tif cs.seeded {\n\t\tb.WriteString(\"_Seeded by a moderator to start this court; the author earns nothing from it._\\n\\n\")\n\t}\n\n\tb.WriteString(\"## Signal\\n\\n\")\n\t// ANSWERED CLAIMS SHOW THE RECORD, NOT THE REMAINDER. The live counters are\n\t// decremented as principal is withdrawn, so a settled claim's page used to\n\t// converge on \"YES 0 / NO 0\" — the drained value, displayed as though it were\n\t// the history. The frozen pair is what the claim was actually answered on.\n\tif cs.frozenAt != 0 {\n\t\tfTotal := cs.yesStakeAtFreeze + cs.noStakeAtFreeze\n\t\tb.WriteString(\"- staked when answered: \" + strconv.FormatInt(fTotal, 10) +\n\t\t\t\" \" + qualifiedSymbol(c) + \" (YES \" + strconv.FormatInt(cs.yesStakeAtFreeze, 10) +\n\t\t\t\" / NO \" + strconv.FormatInt(cs.noStakeAtFreeze, 10) + \")\\n\")\n\t\tif fTotal \u003e 0 {\n\t\t\tb.WriteString(\"- lean when answered: \" + pct(cs.yesStakeAtFreeze, fTotal) + \" YES\\n\")\n\t\t}\n\t\tif live := cs.yesStake + cs.noStake; live \u003e 0 {\n\t\t\tb.WriteString(\"- still unwithdrawn: \" + strconv.FormatInt(live, 10) +\n\t\t\t\t\" \" + qualifiedSymbol(c) + \"\\n\")\n\t\t}\n\t} else {\n\t\ttotal := cs.yesStake + cs.noStake\n\t\tb.WriteString(\"- staked now: \" + strconv.FormatInt(total, 10) +\n\t\t\t\" \" + qualifiedSymbol(c) + \" (YES \" + strconv.FormatInt(cs.yesStake, 10) +\n\t\t\t\" / NO \" + strconv.FormatInt(cs.noStake, 10) + \")\\n\")\n\t\tif total \u003e 0 {\n\t\t\tb.WriteString(\"- right now: \" + pct(cs.yesStake, total) + \" YES\\n\")\n\t\t}\n\t\t// AND WHAT THAT STAKE IS BUYING, which is live information on an\n\t\t// unanswered claim and nowhere else: the draw multiplier is this\n\t\t// claim's total stake against a typical claim here, fixed at the\n\t\t// answer. So the number a staker moves NOW is the one that will be\n\t\t// frozen, and after the answer it can no longer be moved at all.\n\t\tb.WriteString(\"- pays: not set until the claim is answered — it is \" +\n\t\t\t\"this claim's total stake against a typical claim in this court, \" +\n\t\t\t\"so staking now is what sets it\\n\")\n\t}\n\th := heightNow()\n\tif yAvg, ym := cs.yes.Average(h, periodBlocks); ym {\n\t\tif tAvg, tm := cs.oi.Average(h, periodBlocks); tm \u0026\u0026 tAvg \u003e 0 {\n\t\t\tb.WriteString(\"- trailing week: \" + pct(yAvg, tAvg) + \" YES\\n\")\n\t\t}\n\t}\n\tyc := convToCC(cs.yesConvHi, cs.yesConvLo)\n\tnc := convToCC(cs.noConvHi, cs.noConvLo)\n\tif yc+nc \u003e 0 {\n\t\t// The conviction-weighted lifetime ratio — the series a manipulator can\n\t\t// move only by paying capital-time (F9).\n\t\tb.WriteString(\"- lifetime, weighted by how long each stake was held: \" + pct(yc, yc+nc) + \" YES\\n\")\n\t}\n\tb.WriteString(\"\\n## Status\\n\\n\" + claimStatus(cs) + \"\\n\")\n\tif cs.frozenAt != 0 {\n\t\tb.WriteString(\"\\n## Resolution\\n\\n\")\n\t\tb.WriteString(\"- answer: \" + sideName(int(cs.answer)) + \" (bond \" +\n\t\t\tstrconv.FormatInt(cs.answerBond0, 10) + \" \" + qualifiedSymbol(c) + \")\\n\")\n\t\t// The answerer's track record — contested-and-upheld answers only, so a\n\t\t// high number is hard-won signal, not volume (P13). If the court's\n\t\t// priority gate is armed, a record ≥ threshold earns a 24h head start.\n\t\tb.WriteString(\"- the answerer has been challenged and upheld \" +\n\t\t\tstrconv.FormatInt(int64(AnswerRecord(slug, cs.answerer)), 10) +\n\t\t\t\" time(s) in this court — answers nobody contested do not count\\n\")\n\t\tif PriorityGateActive(slug) {\n\t\t\tb.WriteString(\"- answer priority is live here — a record of \" +\n\t\t\t\tstrconv.FormatInt(int64(priorityNetRecord), 10) +\n\t\t\t\t\"+ earns a 24h head start on new claims\\n\")\n\t\t}\n\t\tif cs.failedRounds \u003e 0 {\n\t\t\tb.WriteString(\"- failed dispute rounds: \" + strconv.FormatInt(cs.failedRounds, 10) +\n\t\t\t\t\" of \" + strconv.FormatInt(int64(maxFailedRounds), 10) +\n\t\t\t\t\" — the next dispute bond doubles\\n\")\n\t\t}\n\t\tif cs.disputeOpen {\n\t\t\t// The sealed-tally rule: an open vote renders NO running count.\n\t\t\t//\n\t\t\t// SAID AS WHAT THIS PAGE DOES, not as what a reader cannot have.\n\t\t\t// \"The tally is sealed\" reads as \"the count is hidden\", and it is\n\t\t\t// not: every ballot is a public transaction and anyone may total\n\t\t\t// them. What is true is that this realm will not do the addition\n\t\t\t// while the vote is open, because a running total published here\n\t\t\t// is the thing that makes copying the largest voter the best move.\n\t\t\tb.WriteString(\"- dispute round \" + strconv.FormatInt(cs.round, 10) +\n\t\t\t\t\" vote open — no running total is shown here until it closes\\n\")\n\t\t} else if cs.verdictAt == 0 {\n\t\t\t// Disputable: show what contesting the answer costs right now, so a\n\t\t\t// user can weigh it without simulating the bond formula themselves.\n\t\t\t// The bond doubles each failed round (reflected as rounds accrue).\n\t\t\tb.WriteString(\"- disputing this answer costs a bond of \" +\n\t\t\t\tstrconv.FormatInt(cs.disputeBond0(), 10) + \" \" + qualifiedSymbol(c) +\n\t\t\t\t\", doubling for each round that fails to reach a quorum\\n\")\n\t\t\tb.WriteString(\"  - win and the answerer's bond is burned and you are \" +\n\t\t\t\t\"compensated, up to twice your own bond; lose and yours burns \" +\n\t\t\t\t\"instead. Nothing passes between the two of you — a forfeit is \" +\n\t\t\t\t\"burned, a compensation is newly issued\\n\")\n\t\t\tb.WriteString(\"  - if the round draws no quorum, half your bond burns \" +\n\t\t\t\t\"and half returns; three such rounds close the claim undecided, \" +\n\t\t\t\t\"with every stake out at 1× and the deposit and fee refunded\\n\")\n\t\t\t// THE SENTENCE THAT MATTERS MOST, and the one the 1× guarantee makes\n\t\t\t// true: a dispute risks the BOND and never the stake. Without it a\n\t\t\t// reader weighing a dispute has to assume their position is exposed.\n\t\t\tb.WriteString(\"  - **your stake is not at risk either way** — only the \" +\n\t\t\t\t\"bond is\\n\")\n\t\t}\n\t\tif cs.verdictAt != 0 \u0026\u0026 !cs.provClose {\n\t\t\tb.WriteString(\"- verdict: \" + sideName(int(cs.provisional)) + \" — \" + cs.route + \"\\n\")\n\t\t}\n\t\t// WHAT THE CLAIM PAYS, and what set it.\n\t\t//\n\t\t// This used to print a voted band — low, mid or high — with an offer to\n\t\t// challenge it by posting a bond. Both are gone: the multiplier is the\n\t\t// claim's own size against the court's typical claim (tier.gno), it is\n\t\t// fixed the moment the claim is answered, and no vote can move it. A page\n\t\t// still selling a challenge would be selling a transaction that cannot\n\t\t// change the number it names.\n\t\t//\n\t\t// BEFORE THE ANSWER THERE IS NO MULTIPLIER, and saying one would be worse\n\t\t// than saying none: xBarFrozen is unset, so any figure printed here would\n\t\t// be the floor, i.e. the page would tell a staker their claim currently\n\t\t// pays a quarter when nothing has been decided at all.\n\t\tif cs.tierRef \u003c= 0 {\n\t\t\t// A CLAIM ANSWERED BEFORE THE SIZE RATING SHIPPED has no reference\n\t\t\t// recorded, so tierBpsFor reads it as par. Printing the ratio anyway\n\t\t\t// gave \"held 0 against a typical 0\" — two figures that are missing,\n\t\t\t// not zero, presented as if they were measurements. Say which it is.\n\t\t\tb.WriteString(\"- pays \" + tierText(tierParBps) +\n\t\t\t\t\" — this claim was answered before ratings were recorded, so it \" +\n\t\t\t\t\"pays the standard amount\\n\")\n\t\t} else {\n\t\t\tb.WriteString(\"- pays \" + tierText(tierBpsFor(cs)) +\n\t\t\t\t\" — this claim held \" + strconv.FormatInt(cs.xBarFrozen, 10) +\n\t\t\t\t\" \" + qualifiedSymbol(c) + \" against a typical \" +\n\t\t\t\tstrconv.FormatInt(cs.tierRef, 10) + \" \" + qualifiedSymbol(c) + \" here\\n\")\n\t\t\t// THE RULE, not just the number, because the number alone reads as an\n\t\t\t// editorial rating rather than as the difference in what is paid.\n\t\t\tb.WriteString(\"- the rating is the money that showed up, not a vote: \" +\n\t\t\t\t\"a claim of typical size pays 1.00×, and it is capped at 0.25× and 2.00×\\n\")\n\t\t\t// AND THE SPAM CUT, when a round has decided. Two multipliers reach\n\t\t\t// this claim's draw and the page showed only one, which on a claim a\n\t\t\t// quarter of the voters flagged reported double what it actually pays.\n\t\t\t//\n\t\t\t// BOTH FIGURES, not the product: a reader handed \"0.75x\" cannot tell a\n\t\t\t// large claim the crowd cut down from a small one nobody touched, and\n\t\t\t// those are opposite facts about the same number.\n\t\t\tif cs.spamTotalW \u003e 0 \u0026\u0026 cs.spamW \u003e 0 {\n\t\t\t\tb.WriteString(\"- and \" + tierText(spamNetBps(cs)) +\n\t\t\t\t\t\" of that, because \" + strconv.FormatInt(cs.spamW, 10) +\n\t\t\t\t\t\" of \" + strconv.FormatInt(cs.spamTotalW, 10) +\n\t\t\t\t\t\" of the weight cast flagged it as spam\\n\")\n\t\t\t}\n\t\t}\n\t\tswitch {\n\t\t// THE CHALLENGE OFFER IS GONE. It invited a bond to \"challenge that\n\t\t// rating\", and the rating is now the claim's own size — frozen at the\n\t\t// answer, and not something a vote can move. Selling it would be selling\n\t\t// a transaction that cannot do what the sentence beside it says. The\n\t\t// flag entrypoints still exist and are removed with the lane; the page\n\t\t// stops advertising them here.\n\t\t}\n\t\tif cs.rewardsOpened {\n\t\t\tw, a, ans, carrot := cs.drawWinners, cs.drawAuthor, cs.drawAnswerer, cs.carrotPool\n\t\t\tb.WriteString(\"- rewards open — pools (\" + qualifiedSymbol(c) +\n\t\t\t\t\"): accuracy \" + strconv.FormatInt(w, 10) +\n\t\t\t\t\", author \" + strconv.FormatInt(a, 10) + \", answerer \" + strconv.FormatInt(ans, 10) +\n\t\t\t\t\", participation remaining \" + strconv.FormatInt(carrot, 10) + \"\\n\")\n\t\t}\n\t}\n\tb.WriteString(\"\\n_To see your own stake on this claim, put your address on \" +\n\t\t\"the end of this page's path:_ `/r/kourt/kourtv2:\" + sanitize.InlineText(slug) +\n\t\t\"/\" + strconv.FormatUint(id, 10) + \"/\u003cyour address\u003e`\\n\\n\")\n\t// THE DISCUSSION, LAST. It sat third of five, above the signal and the\n\t// resolution — a reader scrolling for the state of the market waded through\n\t// other people's arguments to reach it. Comments go after the thing being\n\t// discussed, which is where the overlay puts them and where every other\n\t// discussion on the web is.\n\t//\n\t// It also used to be only a LINK, which is the older defect: the board route\n\t// existed and the only way to reach it was to type the URL, so every comment\n\t// ever written was on a page nobody could navigate to.\n\twriteBoardLink(\u0026b, c, cs, id)\n\treturn b.String()\n}\n\n// renderPositions is the per-address \"your page\" for one claim (§11): live\n// principal per side, durable conviction (the reward weight), the caller's\n// roles, and lifecycle hints on what is withdrawable. Read-only — it composes\n// existing reads and never re-derives the money math the pull entrypoints own.\nfunc renderPositions(slug string, id uint64, addrStr string) string {\n\tif !Exists(slug) {\n\t\treturn \"## Not found\\nNo court by that slug.\"\n\t}\n\tc := mustCourt(slug)\n\tif id == 0 || id \u003e c.nextID {\n\t\treturn \"## Not found\\nNo claim by that id.\"\n\t}\n\twho := address(addrStr)\n\tif !who.IsValid() {\n\t\treturn \"## Not found\\nThat is not a valid address.\"\n\t}\n\tcs := mustClaim(c, id)\n\n\tb := strings.Builder{}\n\tb.WriteString(\"# Positions on this claim\\n\\n\")\n\t// Through the SAME text gate as every other surface, with the banner: this\n\t// route (…/\u003cslug\u003e/\u003cid\u003e/\u003caddr\u003e, reachable with any address string) was the one\n\t// render path that bypassed it, so a purged claim's text stayed publicly\n\t// readable and purge — the legal-compliance power — did not actually reach\n\t// \"every surface\".\n\tb.WriteString(hideBanner(c, cs))\n\tb.WriteString(\"Claim: [\" + claimTitleFor(c, cs) + \"](/r/kourt/kourtv2:\" + slug +\n\t\t\"/\" + strconv.FormatUint(id, 10) + \") in [\" + courtNameFor(c) +\n\t\t\"](/r/kourt/kourtv2:\" + slug + \")\\n\\n\")\n\tb.WriteString(\"Address: \" + sanitize.InlineText(addrStr) + \"\\n\\n\")\n\n\tyStake := StakeOf(slug, id, sideYES, who)\n\tnStake := StakeOf(slug, id, sideNO, who)\n\tyConv := ConvictionOf(slug, id, sideYES, who)\n\tnConv := ConvictionOf(slug, id, sideNO, who)\n\tb.WriteString(\"## Stake (what you have in, and always get back)\\n\\n\")\n\tb.WriteString(\"- YES: \" + strconv.FormatInt(yStake, 10) +\n\t\t\"   NO: \" + strconv.FormatInt(nStake, 10) + \" (\" + qualifiedSymbol(c) +\n\t\t\" base units)\\n\")\n\tb.WriteString(\"- conviction so far — YES: \" + strconv.FormatInt(yConv, 10) +\n\t\t\"   NO: \" + strconv.FormatInt(nConv, 10) + \" (the reward weight)\\n\")\n\n\t// The wallet-level picture, which nothing rendered before staking became a\n\t// lock in place. It matters now for a reason that is invisible from the\n\t// balance alone: staked CC STAYS in the holder's balance and keeps voting, so\n\t// a client showing only the balance tells someone they have coins to bond\n\t// when those coins are already committed, and the refusal then reads as a bug\n\t// in the realm rather than as the arithmetic it is.\n\t//\n\t// COURT-WIDE, not claim-scoped, and labelled as such — the numbers above are\n\t// this claim's, these are every claim in this court plus any live bond, so\n\t// showing them side by side without saying which is which would be worse than\n\t// showing neither.\n\t// TWO \"free\" FIGURES, NOT ONE, because there are two gates and they answer\n\t// different questions. A stake draws on spendable() through mustStakable: it\n\t// moves nothing, so a pending vote is no reason to refuse it. Everything else —\n\t// a deposit, a bond, a transfer, a wrap — draws on disposable() through\n\t// mustSpendable, which nets out the vote lock as well.\n\t//\n\t// This line used to read \"free to commit: \u003cspendable\u003e — what a new stake,\n\t// deposit or bond draws on\", which was one number wearing three labels. After\n\t// the vote lock it told a voter they had coin free for a bond that would be\n\t// refused, and named the wrong gate for two of the three uses.\n\t//\n\t// The PARTITION the test cares about survives, twice over:\n\t//   held = committed as stake     + free to stake\n\t//   held = max(stake, voting)     + free to bond/deposit/transfer\n\t// The two locks OVERLAP rather than stack — staked coin still votes, so one\n\t// pile can answer to both — which is why the second row is a max and not a sum.\n\tbal := c.coin.BalanceOf(who)\n\tlocked := lockedOf(c, who)\n\tvoted := voteLockedOf(c, who)\n\tb.WriteString(\"\\n## This address's \" + qualifiedSymbol(c) +\n\t\t\" in this court (all claims)\\n\\n\")\n\tb.WriteString(\"- held: \" + strconv.FormatInt(bal, 10) +\n\t\t\" — every unit votes, committed or not\\n\")\n\tb.WriteString(\"- committed as stake: \" + strconv.FormatInt(locked, 10) + \"\\n\")\n\tb.WriteString(\"- committed by voting: \" + strconv.FormatInt(voted, 10) +\n\t\t\" — until each question resolves\\n\")\n\tb.WriteString(\"- free to stake: \" + strconv.FormatInt(spendable(c, who), 10) + \"\\n\")\n\tb.WriteString(\"- free to bond, deposit or transfer: \" +\n\t\tstrconv.FormatInt(disposable(c, who), 10) + \"\\n\")\n\n\t// Roles on this claim.\n\troles := \"\"\n\tif who == cs.author {\n\t\troles += \"author \"\n\t}\n\tif cs.frozenAt != 0 \u0026\u0026 who == cs.answerer {\n\t\troles += \"answerer \"\n\t}\n\tif roles != \"\" {\n\t\tb.WriteString(\"- roles: \" + roles + \"\\n\")\n\t}\n\tif rec := AnswerRecord(slug, who); rec \u003e 0 {\n\t\tb.WriteString(\"- answers challenged and upheld in this court: \" + strconv.FormatInt(int64(rec), 10) + \"\\n\")\n\t}\n\n\t// Lifecycle hints — what to do next. No amounts (the pulls compute those).\n\tb.WriteString(\"\\n## What you can do\\n\\n\")\n\tswitch {\n\tcase cs.verdictAt == 0 \u0026\u0026 cs.frozenAt == 0:\n\t\tb.WriteString(\"- staking is open — stake or unstake freely until an answer posts\\n\")\n\tcase cs.verdictAt == 0:\n\t\tb.WriteString(\"- answered, awaiting the verdict — principal is never withheld; withdraw once it is final\\n\")\n\tdefault:\n\t\tb.WriteString(\"- the verdict is final — withdraw your principal on either side (1×)\\n\")\n\t\tif cs.rewardsOpened {\n\t\t\twin := int(cs.provisional)\n\t\t\tif !cs.provClose \u0026\u0026 ((win == sideYES \u0026\u0026 yStake+yConv \u003e 0) || (win == sideNO \u0026\u0026 nStake+nConv \u003e 0)) {\n\t\t\t\tb.WriteString(\"- you backed the winning side — pull your accuracy reward\\n\")\n\t\t\t}\n\t\t\tif who == cs.author \u0026\u0026 cs.drawAuthor \u003e 0 {\n\t\t\t\tb.WriteString(\"- author reward available — pull it\\n\")\n\t\t\t}\n\t\t\tif who == cs.answerer \u0026\u0026 cs.drawAnswerer \u003e 0 {\n\t\t\t\tb.WriteString(\"- answerer reward available — pull it\\n\")\n\t\t\t}\n\t\t}\n\t}\n\treturn b.String()\n}\n\n// tierText renders a bps multiplier as a decimal — 14700 becomes \"1.47×\".\n//\n// It replaced tierName, which named three voted bands. There are no bands now;\n// the multiplier is continuous, so a name would have to invent boundaries the\n// arithmetic does not have. Two decimals because the clamps are 0.25 and 2.00\n// and one decimal would render a quarter of par as \"0.2×\".\nfunc tierText(bps int64) string {\n\tif bps \u003c 0 {\n\t\tbps = 0\n\t}\n\tfrac := (bps % tierParBps) / 100\n\tout := strconv.FormatInt(bps/tierParBps, 10) + \".\"\n\tif frac \u003c 10 {\n\t\tout += \"0\"\n\t}\n\treturn out + strconv.FormatInt(frac, 10) + \"×\"\n}\n\nfunc sideName(side int) string {\n\tif side == sideYES {\n\t\treturn \"YES\"\n\t}\n\treturn \"NO\"\n}\n\n// ClaimStatus is the human one-line lifecycle status for a claim — the exact\n// text the claim page shows — exposed so a client gets it in ONE qeval instead\n// of reconstructing the phase precedence from a half-dozen reads.\nfunc ClaimStatus(courtSlug string, claimID uint64) string {\n\treturn claimStatus(mustClaim(mustCourt(courtSlug), claimID))\n}\n\n// claimStatus is the one-line lifecycle readout. It grows verdict/tier/route\n// lines as those modules land; the sealed-tally rule (never a running count)\n// binds every future addition.\nfunc claimStatus(cs *claimState) string {\n\tswitch {\n\tcase cs.closed:\n\t\treturn \"closed — never answered; every stake exited 1×, the deposit refunded\"\n\t// SPAM COMES FIRST, because a discarded claim also carries provClose — that\n\t// flag is how it reuses every withdrawal path that already means \"everyone\n\t// takes their principal back\". Without this case ahead of it the page would\n\t// report a claim the court deliberately THREW OUT as one that merely failed\n\t// to reach quorum three times: the same words for the opposite event, and\n\t// the one reading a reader would use to judge whether the system worked.\n\tcase cs.spamClosed:\n\t\treturn \"discarded — more than half the weight cast called this claim spam; \" +\n\t\t\t\"every stake withdraws 1×, the answerer's and disputer's bonds return, \" +\n\t\t\t\"the filing fee burns\"\n\tcase cs.provClose:\n\t\treturn \"closed without a decision — three dispute rounds failed quorum; everyone withdraws 1×, deposit and fee refunded\"\n\tcase cs.verdictAt != 0:\n\t\t// NAMING THE SIDE IS THE WHOLE POINT OF A VERDICT. This line said only\n\t\t// \"settled\" for a long time, so a settled-YES claim and a settled-NO claim\n\t\t// read identically on every client that shows a one-line status — while the\n\t\t// PROVISIONAL case two branches down has always named its side. The overlay\n\t\t// had to ask Verdict() per settled claim to recover what the status already\n\t\t// knew.\n\t\t//\n\t\t// provisional IS the verdict here, and this reads it under exactly the guard\n\t\t// Verdict() uses: verdictAt != 0 and not provClose, which the case above\n\t\t// already took. It cannot be -1 in this branch — settle-undisputed writes\n\t\t// provisional = answer beside verdictAt, and Finalize's only route in\n\t\t// requires escrowUntil, which is written after a resolution has set\n\t\t// provisional. TestBothSettleRoutesNameTheVerdictSide pins both.\n\t\treturn \"settled \" + sideName(int(cs.provisional)) + \" — every stake withdraws 1×\"\n\tcase cs.disputeOpen:\n\t\t// AND A DISPUTE HAS A SIDE TOO, for the reason the settled branch above\n\t\t// gives about itself: this line named none, so every disputed claim in a\n\t\t// court read identically and a client showing a one-line status could not\n\t\t// say what was being contested. Settled, provisional and answered have all\n\t\t// named their side; disputed was the last one that did not.\n\t\t//\n\t\t// THE SIDE UNDER DISPUTE IS THE ONE ON THE TABLE, which is the provisional\n\t\t// once a round has produced one and the answer before that. A re-dispute\n\t\t// reaches this branch with provisional \u003e= 0 — the case below is only for a\n\t\t// provisional with no dispute running — so reading cs.answer here would\n\t\t// name the original answer while the vote is contesting a later verdict.\n\t\tdisputed := int(cs.answer)\n\t\tif cs.provisional \u003e= 0 {\n\t\t\tdisputed = int(cs.provisional)\n\t\t}\n\t\treturn \"disputed \" + sideName(disputed) +\n\t\t\t\" — a sealed vote is deciding; principal is never withheld\"\n\tcase cs.provisional \u003e= 0:\n\t\treturn \"provisional verdict \" + sideName(int(cs.provisional)) +\n\t\t\t\" — reopenable by a new dispute until block \" + strconv.FormatInt(cs.escrowUntil, 10) +\n\t\t\t\"; the side it is against may withdraw 1× now\"\n\tcase cs.frozenAt != 0:\n\t\treturn \"answered \" + sideName(int(cs.answer)) +\n\t\t\t\" — staking frozen; disputable until block \" +\n\t\t\tstrconv.FormatInt(cs.answerHeight+settleDelay, 10) + \", then it settles undisputed\"\n\tdefault:\n\t\treturn \"open — stake YES or NO; unstake freely until an answer posts\"\n\t}\n}\n\n// pct renders a basis-point-precision percentage from two int64s. num can be a\n// conviction-CC value, which is stake×TIME and so NOT bounded by supply — a bare\n// num*10000 wraps for a whale on a long-open claim (math-audit MED-1). The\n// 128-bit intermediate is overflow-proof (num ≤ den here, so bps ≤ 10000).\nfunc pct(num, den int64) string {\n\tif den \u003c= 0 || num \u003c 0 {\n\t\treturn \"—\"\n\t}\n\tbps := mulDiv128(num, 10000, den)\n\treturn strconv.FormatInt(bps/100, 10) + \".\" + pad2(bps%100) + \"%\"\n}\n\nfunc pad2(v int64) string {\n\tif v \u003c 10 {\n\t\treturn \"0\" + strconv.FormatInt(v, 10)\n\t}\n\treturn strconv.FormatInt(v, 10)\n}\n\n// writeFolderIndex is the court's root folders, on one line, or nothing at all.\n//\n// Retired and purged headings are skipped: a retired folder is one a moderator\n// struck from the tree, and a purged one has had its name erased rather than\n// flagged, so there is no name to print. Both still occupy an id — the overlay\n// walks 1..FolderCount and calls that contiguity load-bearing — which is why\n// this filters on the rows instead of on the count.\nfunc writeFolderIndex(b *strings.Builder, c *Court, slug string) {\n\tif c.mod == nil {\n\t\treturn\n\t}\n\tshown := 0\n\tfor _, f := range c.mod.folderRows() {\n\t\tif f.parent != 0 || f.retired || f.purged {\n\t\t\tcontinue\n\t\t}\n\t\tif shown == 0 {\n\t\t\tb.WriteString(\"Filed under: \")\n\t\t} else {\n\t\t\tb.WriteString(\" · \")\n\t\t}\n\t\tb.WriteString(\"[\" + sanitize.InlineText(f.name) + \"](/r/kourt/kourtv2:\" +\n\t\t\tslug + \"/folder/\" + strconv.FormatUint(f.id, 10) + \")\")\n\t\tshown++\n\t}\n\tif shown \u003e 0 {\n\t\tb.WriteString(\"\\n\\n\")\n\t}\n}\n\n// renderFolderPage draws one heading: what is under it, and what is filed in it.\n//\n// A FOLDER CURATES AND CAN NEVER BURY — that is the founding promise of this\n// module, and it is why this page is safe to render at all. Every claim here is\n// also on the court's flat newest-first docket; nothing is reachable only\n// through a folder, so a moderator cannot use the filing tree to make a claim\n// hard to find. The page says so, because a reader looking at somebody else's\n// organisation of a contested subject is entitled to know it is not the only way\n// in.\nfunc renderFolderPage(slug string, fid uint64) string {\n\tif !Exists(slug) {\n\t\treturn \"## Not found\\nNo court by that slug.\"\n\t}\n\tc := mustCourt(slug)\n\tif c.mod == nil {\n\t\treturn \"## Not found\\nThis court has no folders.\"\n\t}\n\tv := c.mod.folders.Get(beClaimKey(fid))\n\tif v == nil {\n\t\treturn \"## Not found\\nNo folder by that id.\"\n\t}\n\tf := v.(*folder)\n\tbase := \"/r/kourt/kourtv2:\" + slug\n\tvar b strings.Builder\n\t// A PURGED HEADING STILL RENDERS, as a tombstone naming the authority — the\n\t// same posture a purged claim's own page takes. A page that 404s instead\n\t// would make a purge indistinguishable from a folder that never existed.\n\tif f.purged {\n\t\tb.WriteString(\"# [purged:\" + sanitize.InlineText(f.code) + \"]\\n\\n\")\n\t\tb.WriteString(\"_This heading was purged by the global DAO. Its name and \" +\n\t\t\t\"description were erased, not hidden._\\n\\n\")\n\t\tb.WriteString(\"[← \" + courtNameFor(c) + \"](\" + base + \")\\n\")\n\t\treturn b.String()\n\t}\n\tb.WriteString(\"# \" + sanitize.InlineText(f.name) + \"\\n\\n\")\n\t// The breadcrumb walks up, so a nested heading says where it sits rather\n\t// than stranding a reader who arrived by deep link.\n\tb.WriteString(\"[← \" + courtNameFor(c) + \"](\" + base + \")\")\n\tfor up, n := f.parent, 0; up != 0 \u0026\u0026 n \u003c= maxFolders; n++ {\n\t\tpv := c.mod.folders.Get(beClaimKey(up))\n\t\tif pv == nil {\n\t\t\tbreak\n\t\t}\n\t\tpf := pv.(*folder)\n\t\tb.WriteString(\" · [\" + sanitize.InlineText(pf.name) + \"](\" + base +\n\t\t\t\"/folder/\" + strconv.FormatUint(pf.id, 10) + \")\")\n\t\tup = pf.parent\n\t}\n\tb.WriteString(\"\\n\\n\")\n\tif f.retired {\n\t\tb.WriteString(\"\u003e This heading was retired by a moderator. It is kept so \" +\n\t\t\t\"links to it still resolve.\\n\\n\")\n\t}\n\t/* WHO MADE THIS HEADING, which the chain knew and would not say.\n\t *\n\t * A GOVERNED SET RENDERED AS A BARE TITLE. New deliberately leaves the\n\t * description empty — \"this set has a whole claim, with a body, a stake history\n\t * and a verdict, sitting behind it, and that is a better description than a line\n\t * anybody could have written\" — so the page went heading, breadcrumb, and\n\t * straight into the claim list with no words at all. The reasoning is right and\n\t * the consequence was that the better description was never linked to.\n\t *\n\t * AND THIS IS THE SURFACE THAT MATTERS MOST for the distinction, because it is\n\t * the chain's own. The overlay says it three ways now; gnoweb, and anyone\n\t * reading the realm directly, saw a folder and a governed set as the same\n\t * object. bornOf is stored right here on the row.\n\t *\n\t * BEFORE THE DESCRIPTION, not after: on a moderator's folder the desc is the\n\t * first thing said about it, and on a born set there is no desc, so this takes\n\t * that place rather than trailing the claim list. 0 means declared, so a\n\t * moderator's folder reads exactly as it always did.\n\t */\n\tif f.bornOf != 0 {\n\t\tb.WriteString(\"\u003e The court voted this heading into existence: [claim #\" +\n\t\t\tstrconv.FormatUint(f.bornOf, 10) + \"](\" + base + \"/\" +\n\t\t\tstrconv.FormatUint(f.bornOf, 10) + \") settled YES, and carrying it was \" +\n\t\t\t\"a second act anyone could take.\\n\\n\")\n\t}\n\tif f.desc != \"\" {\n\t\tb.WriteString(sanitize.Block(f.desc) + \"\\n\")\n\t}\n\n\tkids := 0\n\tfor _, k := range c.mod.folderRows() {\n\t\tif k.parent != f.id || k.retired || k.purged {\n\t\t\tcontinue\n\t\t}\n\t\tif kids == 0 {\n\t\t\tb.WriteString(\"## Under this heading\\n\\n\")\n\t\t}\n\t\tkids++\n\t\tb.WriteString(\"- [\" + sanitize.InlineText(k.name) + \"](\" + base + \"/folder/\" +\n\t\t\tstrconv.FormatUint(k.id, 10) + \")\\n\")\n\t}\n\tif kids \u003e 0 {\n\t\tb.WriteString(\"\\n\")\n\t}\n\n\tb.WriteString(\"## Claims filed here\\n\\n\")\n\tfiled := 0\n\tfor _, id := range f.items {\n\t\tif id == 0 || id \u003e c.nextID {\n\t\t\tcontinue\n\t\t}\n\t\tcs := mustClaim(c, id)\n\t\tfiled++\n\t\tb.WriteString(\"- [\" + claimTitleFor(c, cs) + \"](\" + base + \"/\" +\n\t\t\tstrconv.FormatUint(id, 10) + \") — \" + claimStatus(cs) + \"\\n\")\n\t}\n\tif filed == 0 {\n\t\tb.WriteString(\"_Nothing filed here yet._\\n\")\n\t}\n\tb.WriteString(\"\\n_A folder curates and can never bury: every claim above is \" +\n\t\t\"also on the court's own newest-first list, and nothing is reachable only \" +\n\t\t\"from here._\\n\\n\")\n\tb.WriteString(helpLink + \"\\n\")\n\treturn b.String()\n}\n"},{"name":"session.gno","body":"// The 72h settle (PLAN §3.1, P4): an answered, UNDISPUTED claim reaches\n// VERDICT_FINAL a fixed delay after its answer — silence settles, safely,\n// because staking froze at the answer and principal never depends on anything\n// that follows. The answerer's bond returns exactly once (a settled claim can\n// still be quality-flagged — quality ≠ verdict — but the verdict itself is\n// final; V2 has no reopen of undisputed verdicts through this path: contesting\n// happens by DISPUTING before the delay lapses).\n\npackage kourtv2\n\n// SettleUndisputed records the answer as the final verdict once the 72h delay\n// has passed with no dispute. Permissionless.\nfunc SettleUndisputed(cur realm, courtSlug string, claimID uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif cs.frozenAt == 0 {\n\t\tpanic(\"kourtv2: there is no answer to settle\")\n\t}\n\tif cs.disputeOpen || cs.round \u003e 0 {\n\t\tpanic(\"kourtv2: this claim was disputed; it settles by vote\")\n\t}\n\tif cs.verdictAt != 0 {\n\t\tpanic(\"kourtv2: already settled\")\n\t}\n\t// The deadline is a date now (clock.gno); the height arithmetic remains the\n\t// fallback for claims answered before the stamps existed. One message\n\t// either way — the caller is told the same fact.\n\tif passed, known := pastDeadline(cs.answeredAtTime, settleSecs); (known \u0026\u0026 !passed) ||\n\t\t(!known \u0026\u0026 heightNow() \u003c cs.answerHeight+settleDelay) {\n\t\tpanic(\"kourtv2: the answer has not aged the 72h settlement minimum\")\n\t}\n\ttouch(c)\n\n\tcs.verdictAt = heightNow()\n\tcs.verdictAtTime = nowTime()\n\tcs.route = \"undisputed\"\n\t// The record froze with the pools; clip its hourly tail to the final week\n\t// now — no later stake op will ever ride a trim past this point.\n\ttrimClaimHistory(c, cs, uint32(cs.frozenAt/epochBlocks)+1)\n\tcs.provisional = cs.answer\n\t// The bond returns WHOLE. Why it is whole rather than partly retained is\n\t// settleAnswerBond's own story (answerbond.gno) and is not repeated here.\n\t//\n\t// WHAT USED TO BE HERE, because deleting it outright would invite somebody to\n\t// reintroduce it: twenty-one lines sizing a slash-sized reserve to keep\n\t// collateralized past settle (audit M3-HIGH-1, v0.40, v0.44), because a\n\t// conclusive-low flag could still land while flags ran until open the rewards.\n\t// Every mechanism that paragraph reasoned about — the quality slot,\n\t// slotConsumed, OpenFlag, ResolveFlag, pendingSlash — went with the quality\n\t// lane, and none of them has a single non-comment line left in this realm.\n\t// It also cited TestSettleRetainsNothingAfterDustLowNoSlash as the fixture\n\t// isolating its load-bearing clause; that test is gone too, surviving only as\n\t// a mention in audit_m3_test.gno's own header.\n\t//\n\t// So it justified a retention this code no longer performs, through symbols a\n\t// reader cannot find, against a fixture that does not run. Two mutation rows\n\t// anchored on its `if !cs.tierFinal \u0026\u0026 !cs.slotConsumed {` and were retired in\n\t// 4cb3969 for the same reason.\n\tif cs.answerBond \u003e 0 {\n\t\tsettleAnswerBond(c, cs)\n\t}\n\treleasePriority(c, cs.answerer, claimID)\n}\n\n// WithdrawStake returns a staker's FULL principal on one side after the\n// verdict is final. Both sides withdraw 1× — losers lose nothing — and this\n// path is unpausable: no flag, no quality vote, nothing downstream may hold\n// principal (P4/V2-6, split settlement). Conviction records survive the\n// withdrawal for the reward draws.\nfunc WithdrawStake(cur realm, courtSlug string, claimID uint64, side int) int64 {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tmustSide(side)\n\t// Withdrawable at VERDICT_FINAL (both sides), or EARLY for the side a\n\t// standing provisional verdict is against (econ F7: their outcome cannot\n\t// improve — principal is 1× regardless and conviction is already frozen —\n\t// and releasing them defuses the multi-round freeze-hostage, A12).\n\tif cs.verdictAt == 0 \u0026\u0026 !(cs.provisional \u003e= 0 \u0026\u0026 side != int(cs.provisional)) {\n\t\tpanic(\"kourtv2: no verdict yet — unstake freely before an answer, withdraw after the verdict\")\n\t}\n\ttouch(c)\n\tadvancePools(c, cs)\n\tp := getPos(c, cs, who, side)\n\tif p.stake \u003c= 0 {\n\t\tpanic(\"kourtv2: nothing staked on that side\")\n\t}\n\taccrue(c, cs, p, side) // settles to the freeze pin (a no-op if already settled)\n\tamount := p.stake\n\tp.stake = 0\n\tif side == sideYES {\n\t\tcs.yesStake -= amount\n\t} else {\n\t\tcs.noStake -= amount\n\t}\n\treleaseStake(c, who, amount)\n\treturn amount\n}\n\n// ------------------------------------------------------------------ reads --\n\n// Settled reports VERDICT_FINAL; Verdict is the final side; VerdictRoute is\n// \"undisputed\" or \"vote\" — the provenance the record exists to carry.\nfunc Settled(courtSlug string, claimID uint64) bool {\n\treturn mustClaim(mustCourt(courtSlug), claimID).verdictAt != 0\n}\n\nfunc Verdict(courtSlug string, claimID uint64) int {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tif cs.verdictAt == 0 {\n\t\tpanic(\"kourtv2: no verdict yet\")\n\t}\n\tif cs.provClose {\n\t\tpanic(\"kourtv2: closed without a decision (three failed dispute rounds)\")\n\t}\n\treturn int(cs.provisional)\n}\n\nfunc VerdictRoute(courtSlug string, claimID uint64) string {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tif cs.verdictAt == 0 {\n\t\tpanic(\"kourtv2: no verdict yet\")\n\t}\n\treturn cs.route\n}\n\n// SettleDeadline is the block height at which an answered, undisputed claim\n// becomes settleable (the answer height + the 72h delay); 0 if unanswered. A UI\n// renders a wall-clock countdown from it, and it is also the first-round dispute\n// window's close.\nfunc SettleDeadline(courtSlug string, claimID uint64) int64 {\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tif cs.frozenAt == 0 {\n\t\treturn 0\n\t}\n\treturn cs.answerHeight + settleDelay\n}\n"},{"name":"sitelink.gno","body":"// The overlay's domain, and the header that routes a gnoweb reader to it.\n//\n// gnoweb serves this realm's own Render() and always will — that is the\n// guarantee the whole design rests on, and nothing here weakens it. But the\n// overlay at kourt.xyz is where most people should be reading, so every gnoweb\n// page carries a link to the same thing over there.\n//\n// ONE REALM-WIDE VALUE, SET BY THE GLOBAL DAO ADMIN, AND BY NOBODY ELSE. Not the\n// meta court, not a court's moderators, and there is no per-court override — the\n// `ladder` and `creditRates` shape of \"realm default, court may override\" is\n// deliberately NOT used here. A court that could point its own pages at a domain\n// of its choosing could route its readers anywhere while wearing the platform's\n// name, and the meta court exists to review claims rather than to own the\n// platform's front door.\n\npackage kourtv2\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// A hostname, and only a hostname: no scheme, no path, no query, no port.\n//\n// THE REALM OWNS THE PATH. Storing a host and building \"https://\u003chost\u003e/#/c/…\"\n// here means the link on a claim page can only ever point at the SAME claim on\n// the overlay. Store a base URL instead and an admin could set\n// \"evil.example/#/c/other/1?\" and every page would deep-link somewhere it does\n// not say it goes. The admin chooses the house; the realm still writes the\n// address on the envelope.\n//\n// The character set is also what makes the value safe to PRINT. Every byte it\n// permits is inert in both of the contexts the banner puts it in: inert as\n// markdown link text (no brackets, parens, backticks, emphasis), and inert as a\n// link destination (no whitespace, no \")\", no \"\u003c\" or \"\u003e\"). So the banner needs\n// no escaper — and must not use one. sanitize.InlineText backslash-escapes \".\"\n// and \"-\", which renders \"kourt.xyz\" correctly as text but writes\n// \"https://kourt\\.xyz/\" into the destination. An escaper applied to the wrong\n// context is not defence in depth; it is a broken link.\n//\n// The defence in depth that IS right here is fail-closed: siteBanner re-checks\n// the stored value and prints nothing if it fails, so a value that ever reaches\n// the variable without passing this function costs a missing header rather than\n// a malformed page.\nconst maxSiteDomainLen = 100\n\n// siteDomain is empty until an admin sets one, and empty is a working state:\n// every page renders exactly as it did before, with no header at all. A realm\n// that only worked once somebody configured a website would be a realm that\n// depends on a website.\nvar siteDomain string\n\n// siteDomainFault names what is wrong with d, or \"\" when nothing is.\n//\n// Split from the panic so the render path can ask the same question without\n// being able to abort a page: a bad stored value must cost the header, never the\n// claim underneath it.\nfunc siteDomainFault(d string) string {\n\tif len(d) \u003c 4 || len(d) \u003e maxSiteDomainLen {\n\t\treturn \"a site domain is 4..100 characters\"\n\t}\n\tif !strings.Contains(d, \".\") {\n\t\treturn \"a site domain needs a dot — it is a hostname, not a word\"\n\t}\n\tif d[0] == '.' || d[0] == '-' || d[len(d)-1] == '.' || d[len(d)-1] == '-' {\n\t\treturn \"a site domain may not begin or end with a dot or a hyphen\"\n\t}\n\tfor i := 0; i \u003c len(d); i++ {\n\t\tc := d[i]\n\t\tok := (c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= '0' \u0026\u0026 c \u003c= '9') || c == '.' || c == '-'\n\t\tif !ok {\n\t\t\treturn \"a site domain is lowercase letters, digits, dots and \" +\n\t\t\t\t\"hyphens only — no scheme, no path, no port\"\n\t\t}\n\t}\n\tif strings.Contains(d, \"..\") {\n\t\treturn \"a site domain may not contain an empty label\"\n\t}\n\treturn \"\"\n}\n\nfunc mustSiteDomain(d string) {\n\tif fault := siteDomainFault(d); fault != \"\" {\n\t\tpanic(\"kourtv2: \" + fault)\n\t}\n}\n\n// SetSiteDomain points every gnoweb page at the overlay. Global DAO admin only.\nfunc SetSiteDomain(cur realm, domain string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\td := ensureGlobalDAO()\n\tif cur.Previous().Address() != d.admin {\n\t\tpanic(\"kourtv2: only the global DAO admin sets the site domain\")\n\t}\n\tmustSiteDomain(domain)\n\tsiteDomain = domain\n\t// Emitted for the reason SetLadderDefault is: this changes every page in the\n\t// realm at once and there is no court to log it against, so the event is the\n\t// only record there can be.\n\tchain.Emit(globalActEvent,\n\t\t\"court\", \"*\",\n\t\t\"claim\", \"0\",\n\t\t\"act\", \"set-site-domain:\"+domain,\n\t\t\"by\", cur.Previous().Address().String(),\n\t\t\"height\", eventHeight(),\n\t)\n}\n\n// ClearSiteDomain takes the header off every page. Same seat.\n//\n// It exists because a domain that lapses is worse than none: a header pointing\n// at a host somebody else now owns is the realm recommending a stranger.\nfunc ClearSiteDomain(cur realm) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\td := ensureGlobalDAO()\n\tif cur.Previous().Address() != d.admin {\n\t\tpanic(\"kourtv2: only the global DAO admin clears the site domain\")\n\t}\n\tif siteDomain == \"\" {\n\t\tpanic(\"kourtv2: no site domain is set\")\n\t}\n\tsiteDomain = \"\"\n\tchain.Emit(globalActEvent,\n\t\t\"court\", \"*\",\n\t\t\"claim\", \"0\",\n\t\t\"act\", \"clear-site-domain\",\n\t\t\"by\", cur.Previous().Address().String(),\n\t\t\"height\", eventHeight(),\n\t)\n}\n\n// SiteDomain is the configured overlay host, or \"\" when there is none.\nfunc SiteDomain() string { return siteDomain }\n\n// sitePath maps a gnoweb render path onto the overlay's own routing.\n//\n// The overlay is a hash-routed single page — \"#/\", \"#/c/\u003cslug\u003e\",\n// \"#/c/\u003cslug\u003e/\u003cid\u003e\", \"#/me\" — so the mapping is small and total. Anything this\n// realm serves that the overlay has no route for resolves to the nearest thing\n// it does: a board or a row goes to its CLAIM, a moderation log to its COURT.\n// That is a deliberate choice over linking to a route that does not exist —\n// sending a reader to the claim they were reading about is right even before the\n// overlay learns to show comments, and it stays right afterwards.\nfunc sitePath(path string) string {\n\tparts := strings.SplitN(path, \"/\", 4)\n\tif path == \"\" || parts[0] == \"\" {\n\t\treturn \"#/\"\n\t}\n\tif len(parts) == 1 {\n\t\t// The help page is the realm's own; the overlay has no route for it and\n\t\t// linking to \"#/c/how-it-works\" would send a reader to a court that\n\t\t// cannot exist. The directory is the nearest true thing.\n\t\tif parts[0] == helpPath {\n\t\t\treturn \"#/\"\n\t\t}\n\t\t// The parameters page differs from help in the one way that matters: the\n\t\t// overlay HAS a counterpart for it, so the link can be exact rather than\n\t\t// nearest-true. Named here because without an arm it falls through to\n\t\t// \"#/c/admin-params\" — the court route, for a court that cannot exist,\n\t\t// which is the very trap the comment above was written about. It shipped\n\t\t// that way and a live page carried the broken link until this was added.\n\t\tif parts[0] == adminParamsPath {\n\t\t\treturn \"#/params\"\n\t\t}\n\t\treturn \"#/c/\" + parts[0]\n\t}\n\tswitch parts[1] {\n\tcase \"me\":\n\t\treturn \"#/me\"\n\tcase \"mod\":\n\t\treturn \"#/c/\" + parts[0]\n\t}\n\t// A numeric second segment is a claim; everything under it (board, a row,\n\t// a positions page) belongs to that claim.\n\tif _, err := strconv.ParseUint(parts[1], 10, 64); err != nil {\n\t\treturn \"#/c/\" + parts[0]\n\t}\n\treturn \"#/c/\" + parts[0] + \"/\" + parts[1]\n}\n\n// withSiteBanner puts the overlay link UNDER the page's title, not above it.\n//\n// It used to be the first line of every page, which put a link to somewhere else\n// ahead of the name of the thing a reader had just opened. Below the heading it\n// reads as what it is — the same page, better rendered — rather than as an\n// interruption before the page has said what it is.\n//\n// STILL EVERY PAGE, OR NONE. The placement moved; the rule did not. A link that\n// appears on some pages and not others is one a reader cannot rely on, so this\n// inserts after the first heading wherever there is one and falls back to the\n// top where there is not (a \"## Not found\" body, for instance, has no H1).\nfunc withSiteBanner(body, path string) string {\n\tbar := siteBanner(path)\n\tif bar == \"\" {\n\t\treturn body\n\t}\n\tif !strings.HasPrefix(body, \"# \") {\n\t\treturn bar + body\n\t}\n\t// The heading runs to its first newline; everything after it is the page.\n\ti := strings.Index(body, \"\\n\")\n\tif i \u003c 0 {\n\t\treturn body + \"\\n\\n\" + bar\n\t}\n\treturn body[:i+1] + \"\\n\" + bar + strings.TrimLeft(body[i+1:], \"\\n\")\n}\n\n// siteBanner is the header itself, empty when no domain is configured.\n//\n// It sits beside testClockBanner in Render() for the same reason that one does:\n// a disclosure that appears on some pages and not others is a disclosure a\n// reader cannot rely on. First thing, every page, or not at all.\nfunc siteBanner(path string) string {\n\t// Fail closed. No domain is the ordinary case; a domain that does not\n\t// validate is the one that should never happen, and both cost exactly the\n\t// header. See the note above on why this is the right guard and an escaper\n\t// is not.\n\tif siteDomain == \"\" || siteDomainFault(siteDomain) != \"\" {\n\t\treturn \"\"\n\t}\n\treturn \"[Read this on \" + siteDomain + \" →](https://\" + siteDomain + \"/\" +\n\t\tsitePath(path) + \")\\n\\n\"\n}\n"},{"name":"stake.gno","body":"// No-loss conviction staking (PLAN §3.1/§3.2). Stake CC on YES or NO; unstake\n// freely until an answer freezes the claim; principal always returns 1×. The\n// reward weight is CONVICTION: the exact integral ∫ rate(t)·stake·dt, held per\n// (staker, side) in 128 bits — rate-weighted (V2-8), so no rate snapshot exists\n// anywhere and step-downs cannot be raced. Unstaking FORFEITS the credit the\n// withdrawn coin earned, in proportion — you must still be holding when the\n// claim is judged to be paid for it. That reverses F9's capital conservation and\n// the reason is in Unstake. Principal is untouched: no-loss is a promise about\n// principal, and it still holds exactly.\n\npackage kourtv2\n\nimport (\n\t\"math/bits\"\n\t\"math/overflow\"\n)\n\n// r0WeeklyBps is the frozen external-opportunity estimate (ECONOMICS.md):\n// ~13%/yr ≈ 25 bps/week. Part of the published rate rule, never a lever.\nconst r0WeeklyBps = int64(25)\n\n// stepDownNum/Den approximate 2^(−1/104) ≈ 0.9933575: the per-period budget\n// factor of the amortized step-down (no cliff exists to race — 2A-T3).\nconst (\n\tstepDownNum = int64(9_933_575)\n\tstepDownDen = int64(10_000_000)\n)\n\n// budgetBpsFPStart is the weekly budget in bps × 1e6 fixed point (38 bps).\nconst budgetBpsFPStart = budgetWeeklyBps * 1_000_000\n\n// stakePos is one staker's position on one side of one claim.\ntype stakePos struct {\n\tstake       int64  // live staked amount\n\tconvHi      uint64 // rate-weighted conviction, u128: Σ stake × ΔrateAcc\n\tconvLo      uint64\n\tlastRateAcc int64 // the court's era integral at the last settle (audit H1)\n\t// The RAW integral ∫stake·dt (stake × blocks, u128), freeze-capped — the\n\t// F9 flash-proof cap base: time-averaged stake = raw / claim-open-blocks.\n\t// Kept separate from conviction because the cap must not scale with the\n\t// rate (Q7).\n\trawHi, rawLo uint64\n\tlastRawAt    int64\n\tbonusPaid    bool // the winner draw is one-shot (openrewards.gno)\n}\n\n// rateBpsFP is the current published rate in bps × 1e6 per week:\n// rate = 0.85 × y*, y* = 2(r₀ + d_eff)·T_L/T_c with T_L/T_c = 1.5 → 2.55(r₀ + d_eff).\nfunc rateBpsFP(c *Court) int64 {\n\treturn 255 * (r0WeeklyBps*1_000_000 + c.dEffBpsFP) / 100\n}\n\n// effectiveRateAcc is the era integral a claim's positions settle against:\n// the court's cumulative Σ rate×blocks, capped at the claim's freeze snapshot\n// so frozen conviction can never be repriced by later d_eff moves (audit H1).\n// Callers must touch(c) first so the integral is current.\nfunc effectiveRateAcc(c *Court, cs *claimState) int64 {\n\tacc := c.rateAccFP\n\tif cs.frozenAt != 0 \u0026\u0026 cs.rateAccAtFreeze \u003c acc {\n\t\tacc = cs.rateAccAtFreeze\n\t}\n\treturn acc\n}\n\n// accrue settles a position against the era integral: conv += stake × Δacc,\n// where Δacc = Σ rate(t)·dt over exactly the blocks the stake was live — the\n// rate-weighted integral by construction (V2-8; no rate snapshot, no\n// retroactive repricing). Also feeds the per-side pool conviction (§3.9) and\n// the raw ∫stake·dt integral (the Q7 cap base), both freeze-capped.\nfunc accrue(c *Court, cs *claimState, p *stakePos, side int) {\n\tacc := effectiveRateAcc(c, cs)\n\tdelta := acc - p.lastRateAcc\n\tp.lastRateAcc = acc\n\tif delta \u003e 0 \u0026\u0026 p.stake \u003e 0 {\n\t\t// stake \u003c 2^50, Δacc \u003c 2^57 over a century: one 64×64 multiply covers it.\n\t\thi, lo := bits.Mul64(uint64(p.stake), uint64(delta))\n\t\tp.convHi, p.convLo = add128(p.convHi, p.convLo, hi, lo)\n\t}\n\trh := rawHeight(cs)\n\trawDelta := rh - p.lastRawAt\n\tp.lastRawAt = rh\n\tif rawDelta \u003e 0 \u0026\u0026 p.stake \u003e 0 {\n\t\thi, lo := bits.Mul64(uint64(p.stake), uint64(rawDelta))\n\t\tp.rawHi, p.rawLo = add128(p.rawHi, p.rawLo, hi, lo)\n\t}\n\t_ = side\n}\n\n// advancePools settles the per-side POOL integrals analytically from the side\n// totals — sideStake × Δ over exactly the interval since the last mutation,\n// during which the total was constant. This makes the pool EXACT at any\n// moment (open the rewards's denominator) with no dependence on when individual\n// positions settle: Σ position integrals == the side integral by construction.\n// MUST run before any yesStake/noStake mutation.\nfunc advancePools(c *Court, cs *claimState) {\n\tacc := effectiveRateAcc(c, cs)\n\trh := rawHeight(cs)\n\tif d := acc - cs.yesLastAcc; d \u003e 0 \u0026\u0026 cs.yesStake \u003e 0 {\n\t\thi, lo := bits.Mul64(uint64(cs.yesStake), uint64(d))\n\t\tcs.yesConvHi, cs.yesConvLo = add128(cs.yesConvHi, cs.yesConvLo, hi, lo)\n\t}\n\tif d := acc - cs.noLastAcc; d \u003e 0 \u0026\u0026 cs.noStake \u003e 0 {\n\t\thi, lo := bits.Mul64(uint64(cs.noStake), uint64(d))\n\t\tcs.noConvHi, cs.noConvLo = add128(cs.noConvHi, cs.noConvLo, hi, lo)\n\t}\n\tif d := rh - cs.yesLastRawAt; d \u003e 0 \u0026\u0026 cs.yesStake \u003e 0 {\n\t\thi, lo := bits.Mul64(uint64(cs.yesStake), uint64(d))\n\t\tcs.yesRawHi, cs.yesRawLo = add128(cs.yesRawHi, cs.yesRawLo, hi, lo)\n\t}\n\tif d := rh - cs.noLastRawAt; d \u003e 0 \u0026\u0026 cs.noStake \u003e 0 {\n\t\thi, lo := bits.Mul64(uint64(cs.noStake), uint64(d))\n\t\tcs.noRawHi, cs.noRawLo = add128(cs.noRawHi, cs.noRawLo, hi, lo)\n\t}\n\tcs.yesLastAcc, cs.noLastAcc = acc, acc\n\tcs.yesLastRawAt, cs.noLastRawAt = rh, rh\n}\n\n// rawHeight is the height the raw integral runs to: now, capped at the freeze\n// (stake stopped counting toward the cap base when the answer landed).\nfunc rawHeight(cs *claimState) int64 {\n\th := heightNow()\n\tif cs.frozenAt != 0 \u0026\u0026 cs.frozenAt \u003c h {\n\t\th = cs.frozenAt\n\t}\n\treturn h\n}\n\n// lifeAvgStake is the claim's LIFETIME time-averaged TOTAL stake:\n// (∫yesStake·dt + ∫noStake·dt) / (now − openedAt), read from the raw integrals\n// advancePools already keeps — the same integral, over the same denominator,\n// that open the rewards's capBonus uses for the F9 flash-proof base. Callers must\n// have settled the pools to `now` first. The denominator runs from the claim's\n// OPEN, not its first stake, so an idle head only ever lowers it: conservative\n// by construction. Unlike the trailing ring, no withdrawal can lower this —\n// moving it costs capital × TIME, and past capital-time is immutable.\nfunc lifeAvgStake(cs *claimState, now int64) int64 {\n\thi, lo := add128(cs.yesRawHi, cs.yesRawLo, cs.noRawHi, cs.noRawLo)\n\tden := now - cs.openedAt\n\tif den \u003c 1 {\n\t\tden = 1\n\t}\n\treturn div128(hi, lo, den)\n}\n\n// Stake backs one side of a claim with CC. Free to reverse until the answer.\nfunc Stake(cur realm, courtSlug string, claimID uint64, side int, amount int64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tmustSide(side)\n\tif amount \u003c= 0 {\n\t\tpanic(\"kourtv2: stake must be positive\")\n\t}\n\tif cs.closed {\n\t\tpanic(\"kourtv2: this claim is closed\")\n\t}\n\tif cs.frozenAt != 0 {\n\t\tpanic(\"kourtv2: staking is frozen — this claim has an answer\")\n\t}\n\t// Polish window (§9): staking is closed for stakeOpenDelayBlocks after\n\t// OpenClaim so the author/moderators can fix wording before anyone bets.\n\t// Default 0 → no window, behaviour unchanged for courts that don't opt in.\n\t//\n\t// THE SAME WINDOW EditClaimTitle READS (claim.gno), converted the same way\n\t// and deliberately character-identical apart from the polarity: that gate\n\t// asks whether the window has CLOSED, this one whether it is still OPEN. They\n\t// must agree on the instant, because it is one window seen from two sides —\n\t// the title freezes exactly when staking opens. blocksToSecs is what makes\n\t// them agree, the window being a court parameter denominated in blocks.\n\tif passed, known := pastDeadline(cs.openedAtTime, blocksToSecs(c.params.stakeOpenDelayBlocks)); (known \u0026\u0026 !passed) ||\n\t\t(!known \u0026\u0026 heightNow() \u003c cs.openedAt+c.params.stakeOpenDelayBlocks) {\n\t\tpanic(\"kourtv2: the polish window is open; staking has not started yet\")\n\t}\n\ttouch(c)\n\n\t// A LOCK, not a transfer (lock.gno): the coins stay here and keep voting. So\n\t// this is the ONE mustSpendable caller that uses the stake-only check — nothing\n\t// leaves the balance, and a pending vote is no reason to refuse a stake.\n\tmustStakable(c, who, amount)\n\tlockStake(c, who, amount)\n\tadvancePools(c, cs)\n\tp := getPos(c, cs, who, side)\n\taccrue(c, cs, p, side)\n\tp.stake = mustAdd(p.stake, amount)\n\tif side == sideYES {\n\t\tcs.yesStake = mustAdd(cs.yesStake, amount)\n\t} else {\n\t\tcs.noStake = mustAdd(cs.noStake, amount)\n\t}\n\tobservePools(cs)\n\trecordPools(c, cs)\n}\n\n// Unstake withdraws principal from a live claim, and the withdrawn coin gives\n// back the conviction it earned. See the block below for the attack that forced\n// this and for why the RAW integral is deliberately not touched.\nfunc Unstake(cur realm, courtSlug string, claimID uint64, side int, amount int64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tmustSide(side)\n\tif amount \u003c= 0 {\n\t\tpanic(\"kourtv2: unstake must be positive\")\n\t}\n\tif cs.frozenAt != 0 {\n\t\tpanic(\"kourtv2: stakes are frozen — withdraw at settlement (principal is never withheld)\")\n\t}\n\ttouch(c)\n\n\tadvancePools(c, cs)\n\tp := getPos(c, cs, who, side)\n\tif p.stake \u003c amount {\n\t\tpanic(\"kourtv2: unstaking more than is staked\")\n\t}\n\taccrue(c, cs, p, side)\n\t// WITHDRAWING GIVES BACK THE CREDIT THE WITHDRAWN COIN EARNED.\n\t//\n\t// This reverses F9's \"unstake keeps conviction\", and the reason is an attack\n\t// answer.gno measured and priced: conviction is stake x time and used to\n\t// survive a withdrawal, while the answer bond is sized off a THREE-HOUR\n\t// trailing average of what is staked now. So a mill could bank conviction\n\t// with a large stake, drain to the floor two hours before answering, and post\n\t// a bond 224x smaller than the penalty it had earned — ~51 CC against 11,220\n\t// CC of exposure. What made that unprofitable was the quality lane's slash,\n\t// which is being removed.\n\t//\n\t// Rather than re-pricing the bond, the mismatch is closed at its source: the\n\t// reward is priced on history and the bond on the present, so history stops\n\t// counting coin that is no longer present. You must be holding when the claim\n\t// is judged to earn anything, and holding longer is worth more — which is\n\t// also the rule a reader would guess.\n\t//\n\t// PROPORTIONAL, not all-or-nothing, and that is not politeness. A flag for\n\t// \"did you ever exit\" would be a fake fix: stake 1000, unstake 999, keep 1,\n\t// and an exit flag never fires while the position keeps every unit of credit\n\t// the 1000 earned. The credit has to follow the coin.\n\t//\n\t// NO-LOSS IS UNTOUCHED. Principal comes back 1x exactly as before; what is\n\t// forfeited is the REWARD the withdrawn coin was accruing. No-loss has always\n\t// been a promise about principal.\n\t//\n\t// CONVICTION ONLY — NOT THE RAW INTEGRAL, and the asymmetry is the design.\n\t// A pre-existing test names it: X̄frozen is \"the drain-resistant lifetime\n\t// base\". The raw integral prices every %-of-X̄ bar in the system — the answer\n\t// bond, the dispute bond, the quorum floor, the tier reference — and if\n\t// withdrawing shrank it, draining would make a claim CHEAPER to answer and\n\t// cheaper to dispute. That is the attack pointing the other way. Scaling it\n\t// here broke that test on the first pass, which is what the test is for.\n\t//\n\t//\tconviction  = what the coin EARNED, and it follows the coin\n\t//\traw integral = what the claim WAS, and history cannot be withdrawn\n\t//\n\t// THE SIDE POOL TOO. Position conviction and the side pool advance\n\t// independently — the pool from cs.yesStake, the position from p.stake — so\n\t// \"the side equals the sum of its positions\" only survives if the forfeit\n\t// comes out of both. Left in the side, the pot would be sized on conviction\n\t// no position can claim and every honest staker's share diluted by a ghost.\n\tif amount \u003e 0 \u0026\u0026 p.stake \u003e 0 {\n\t\tkeep := p.stake - amount\n\t\t// Per-unit first, then rebuild: conviction is stake x (accumulator\n\t\t// delta), so dividing by the stake leaves the delta — an int64 quantity\n\t\t// well inside div128's guard. Scaling the 128-bit product directly would\n\t\t// need a 192-bit intermediate.\n\t\tconvPer := div128(p.convHi, p.convLo, p.stake)\n\t\tnCHi, nCLo := bits.Mul64(uint64(convPer), uint64(keep))\n\t\t// The floor of the per-unit divide stays with the POSITION and is never\n\t\t// handed to the pool: rounding must not be able to leave the side short.\n\t\tdCHi, dCLo := sub128(p.convHi, p.convLo, nCHi, nCLo)\n\t\tp.convHi, p.convLo = nCHi, nCLo\n\t\tif side == sideYES {\n\t\t\tcs.yesConvHi, cs.yesConvLo = sub128(cs.yesConvHi, cs.yesConvLo, dCHi, dCLo)\n\t\t} else {\n\t\t\tcs.noConvHi, cs.noConvLo = sub128(cs.noConvHi, cs.noConvLo, dCHi, dCLo)\n\t\t}\n\t}\n\tp.stake -= amount\n\treleaseStake(c, who, amount)\n\tif side == sideYES {\n\t\tcs.yesStake -= amount\n\t} else {\n\t\tcs.noStake -= amount\n\t}\n\tobservePools(cs)\n\trecordPools(c, cs)\n}\n\n// observePools records both rings at the current height (§3.9).\nfunc observePools(cs *claimState) {\n\th := heightNow()\n\tcs.oi = cs.oi.Observe(h, mustAdd(cs.yesStake, cs.noStake))\n\tcs.yes = cs.yes.Observe(h, cs.yesStake)\n}\n\n// ------------------------------------------------------------------ reads --\n\n// StakeOf is a staker's live principal on one side.\nfunc StakeOf(courtSlug string, claimID uint64, side int, who address) int64 {\n\tmustSide(side)\n\tcs := mustClaim(mustCourt(courtSlug), claimID)\n\tif v := cs.stakers.Get(posKey(who, side)); v != nil {\n\t\treturn v.(*stakePos).stake\n\t}\n\treturn 0\n}\n\n// ConvictionOf is a staker's accrued rate-weighted conviction on one side,\n// settled to now, returned as the CC-unit draw base (conv / (periodBlocks×1e10)\n// — the same divisor emission.gno pays with), so clients can preview rewards.\nfunc ConvictionOf(courtSlug string, claimID uint64, side int, who address) int64 {\n\tmustSide(side)\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tv := cs.stakers.Get(posKey(who, side))\n\tif v == nil {\n\t\treturn 0\n\t}\n\treturn convPreviewCC(cs, v.(*stakePos), ratePreview(c))\n}\n\n// ratePreview is the era integral as a READ may see it: the stored value plus the\n// un-walked tail at the current rate. Reads must not mutate, so the tail is an\n// approximation that resolves exactly at the next touch.\n//\n// SPLIT OUT so a reader that answers for MANY positions computes it once. It is\n// court-level and identical for every position in a walk, and StakedPage hoists it\n// out of its loop for exactly that reason.\nfunc ratePreview(c *Court) int64 {\n\tacc := c.rateAccFP\n\tif tail := heightNow() - c.lastAccrual; tail \u003e 0 {\n\t\tacc = mustAdd(acc, mustMul(rateBpsFP(c), tail))\n\t}\n\treturn acc\n}\n\n// convPreviewCC settles ONE position against that preview and returns the CC-unit draw\n// base — the same figure ConvictionOf has always answered, now in one place because two\n// readers need it. Duplicating these six lines is the shape openrewards.gno records\n// getting caught for: the mutation harness reported four rows as matching TWICE because\n// PullSenior and SeniorEntitlement each carried their own copy of one formula.\nfunc convPreviewCC(cs *claimState, p *stakePos, acc int64) int64 {\n\tif cs.frozenAt != 0 \u0026\u0026 cs.rateAccAtFreeze \u003c acc {\n\t\tacc = cs.rateAccAtFreeze\n\t}\n\thi, lo := p.convHi, p.convLo\n\tif delta := acc - p.lastRateAcc; delta \u003e 0 \u0026\u0026 p.stake \u003e 0 {\n\t\tmh, ml := bits.Mul64(uint64(p.stake), uint64(delta))\n\t\thi, lo = add128(hi, lo, mh, ml)\n\t}\n\treturn convToCC(hi, lo)\n}\n\n// convToCC converts a conviction accumulator to CC base units:\n// conv / (periodBlocks × 10000 × 1e6). Floor division via 128/64.\nfunc convToCC(hi, lo uint64) int64 {\n\tden := uint64(periodBlocks) * 10_000 * 1_000_000\n\tif hi \u003e= den {\n\t\tpanic(\"kourtv2: conviction exceeds payable range\") // unreachable at bounded magnitudes\n\t}\n\tq, _ := bits.Div64(hi, lo, den)\n\treturn int64(q)\n}\n\n// ----------------------------------------------------------------- innards --\n\nfunc getPos(c *Court, cs *claimState, who address, side int) *stakePos {\n\tk := posKey(who, side)\n\tif v := cs.stakers.Get(k); v != nil {\n\t\treturn v.(*stakePos)\n\t}\n\tp := \u0026stakePos{lastRateAcc: c.rateAccFP, lastRawAt: rawHeight(cs)}\n\tcs.stakers.Set(k, p)\n\t// The address-first index, maintained at the one place a position is created.\n\t// getPos is reached before the guards in Unstake and WithdrawStake, so a\n\t// non-staker can arrive here — the whole message reverts in that case, which is\n\t// what keeps the index and cs.stakers in step. See stakeindex.gno.\n\tindexPosition(c, cs.id, who, side)\n\treturn p\n}\n\nfunc posKey(who address, side int) string {\n\tif side == sideYES {\n\t\treturn string(who) + \"|y\"\n\t}\n\treturn string(who) + \"|n\"\n}\n\nfunc mustSide(side int) {\n\tif side != sideYES \u0026\u0026 side != sideNO {\n\t\tpanic(\"kourtv2: side is 0 (YES) or 1 (NO)\")\n\t}\n}\n\n// mustMul is the checked multiply (gno wraps silently — audit C1's class).\nfunc mustMul(a, b int64) int64 {\n\tm, ok := overflow.Mul64(a, b)\n\tif !ok {\n\t\tpanic(\"kourtv2: multiply overflow\")\n\t}\n\treturn m\n}\n\n// mulDiv128 computes a×b/den with a 128-bit intermediate (audit C1: the naive\n// uint64 product wraps at real-court magnitudes). Floors; panics if the\n// quotient exceeds int64 (unreachable at our bounded inputs).\nfunc mulDiv128(a, b, den int64) int64 {\n\tif a \u003c 0 || b \u003c 0 || den \u003c= 0 {\n\t\tpanic(\"kourtv2: mulDiv128 domain\")\n\t}\n\thi, lo := bits.Mul64(uint64(a), uint64(b))\n\tif hi \u003e= uint64(den) {\n\t\tpanic(\"kourtv2: mulDiv128 quotient overflow\")\n\t}\n\tq, _ := bits.Div64(hi, lo, uint64(den))\n\t// Honor the contract (math-audit NOTE-6): the hi\u003cden guard only bounds q to\n\t// uint64; a quotient in (MaxInt64, 2^64) would silently wrap through\n\t// int64(q). Fail loud instead — unreachable at bounded inputs, but the\n\t// discipline is fail-loud, never silent-wrong.\n\tif q \u003e 9223372036854775807 {\n\t\tpanic(\"kourtv2: mulDiv128 quotient exceeds int64\")\n\t}\n\treturn int64(q)\n}\n\n// mustAdd is the checked add every pool total goes through (gno wraps silently).\nfunc mustAdd(a, b int64) int64 {\n\ts, ok := overflow.Add64(a, b)\n\tif !ok {\n\t\tpanic(\"kourtv2: amount overflow\")\n\t}\n\treturn s\n}\n\n// add128 adds two u128s, panicking on 128-bit overflow (unreachable at our\n// bounded magnitudes; checked anyway — the V1 discipline).\n// sub128 removes b from a. Underflow is a bug, not a clamp: every caller is\n// taking back conviction it can prove was added, so a borrow out of the top word\n// means the side accumulator and its positions have already disagreed.\nfunc sub128(aHi, aLo, bHi, bLo uint64) (hi, lo uint64) {\n\tif aHi \u003c bHi || (aHi == bHi \u0026\u0026 aLo \u003c bLo) {\n\t\tpanic(\"kourtv2: 128-bit conviction underflow\")\n\t}\n\tlo = aLo - bLo\n\thi = aHi - bHi\n\tif aLo \u003c bLo {\n\t\thi--\n\t}\n\treturn hi, lo\n}\n\nfunc add128(aHi, aLo, bHi, bLo uint64) (hi, lo uint64) {\n\tlo = aLo + bLo\n\tcarry := uint64(0)\n\tif lo \u003c aLo {\n\t\tcarry = 1\n\t}\n\thi = aHi + bHi + carry\n\tif hi \u003c aHi || (hi == aHi \u0026\u0026 carry == 1 \u0026\u0026 bHi == ^uint64(0)) {\n\t\tpanic(\"kourtv2: 128-bit conviction overflow\")\n\t}\n\treturn hi, lo\n}\n\n// runtimeHeight exposes the chain height to in-package tests.\n"},{"name":"stakeindex.gno","body":"package kourtv2\n\n// THE ADDRESS-FIRST STAKE INDEX — \"which claims do I hold a position on\", in one read.\n//\n// THE PROBLEM IT REMOVES. cs.stakers is keyed addr|side INSIDE each claimState\n// (claim.gno), so it answers \"who stakes this claim\" and cannot answer \"which claims\n// does this address stake\". A client had to visit every claim and look: web/index.html\n// probed four reads per claim id over a window of the newest SCAN_DEPTH = 100 claims per\n// court, and then told the holder \"older positions and older pulls are not found\n// either\". On a court with more claims than that window, a holder could not see their\n// own position at all. That is not a slow page, it is a page that cannot answer the\n// question — about their own money.\n//\n// WHY A PACKED BUCKET AND NOT A ROW PER POSITION. Measured, on this tree: a row per\n// (holder, claim, side) costs ~465 bytes and ~60,195 gas; the packed form costs ~57\n// bytes and ~36,190 gas, so the naive shape is 8x the storage for a worse read. The\n// mechanic is checkpoint.gno's, which argues it in writing: gno stores a []int64 as\n// TypedValues at ~40 bytes an element and a string at one byte each, and appending to a\n// *struct's field dirties exactly one object instead of doing a tree Set. It is also\n// cheaper to READ — one object load rather than k lazily-loaded leaf values.\n//\n// This matters more than it sounds because the three expensive-looking writes a stake\n// already makes are free in storage terms: observePools rebuilds 2,688 bytes of twap\n// rings IN PLACE at zero net bytes, and advancePools, touch and accrue are int64 fields\n// on objects already dirty. The only permanent storage a marginal position pays is its\n// own stakers row, so an index row would have been +30% on it rather than the \"marginal\"\n// the first draft of this claimed. The packed bucket is +6%.\n//\n// BUCKETED BY CLAIM ID so no single value grows without bound, exactly as\n// checkpoint.gno pages by epoch. A holder with ten thousand positions would otherwise\n// carry one 90 kB string that every stake loads whole.\n//\n// COMPLETE BY CONSTRUCTION, which is why there is no backfill and no coverage flag. The\n// tree is created eagerly in the Court literal beside claims, records, locked and\n// voteLocks, so every court has it from birth; claimState's own header records that this\n// is fresh code with no live state. An index that only covered positions opened after\n// some height would reproduce the failure above one layer down, and invisibly.\n//\n// ONE SPELLING OF SIDE. The entry's side byte is posKey's 'y'/'n' (stake.gno), not a\n// second 0/1 encoding, because two encodings of one fact in two structures that must\n// agree is the shape this design has spent a dozen commits pinning against.\n\n// stakeIdxPage is how many claim ids share one packed bucket.\nconst stakeIdxPage = 64\n\n// stakeIdxEntry is 8 big-endian bytes of claim id then one side byte.\nconst stakeIdxEntry = 9\n\n// stakePageMax caps a single read, on the house rule that no read's cost may grow\n// without bound with attacker-priced writes (stakeseries.gno's seriesRowCap, audit N3).\n// The cap is on the HOLDER'S OWN positions rather than on all claims, which is the\n// difference between this and the client scan it replaces.\nconst stakePageMax = 200\n\n// stakeRow is one packed bucket of one holder's positions, in the order they were\n// opened.\n//\n// APPEND-ONLY, WHICH IS A CONTRACT AND NOT AN ACCIDENT. Entries are never reordered or\n// removed, so any prefix of a holder's list is stable across reads and an `offset` a\n// client held from an earlier page still points at the same entry. Keeping each bucket\n// sorted by claim id was the first design and it is worse: inserting a lower id shifts\n// everything after it, so a client paging through positions would silently re-read or\n// skip entries whenever the holder opened a position on an older claim. Buckets are\n// visited in claim-id order; entries within a bucket are in the order they arrived.\ntype stakeRow struct {\n\tpacked string // [claimID:8 BE][side:1 'y'|'n'] entries, 9 bytes each\n}\n\nfunc stakeIdxKey(who address, claimID uint64) string {\n\treturn string(who) + beClaimKey(claimID/stakeIdxPage)\n}\n\nfunc sideByte(side int) string {\n\tif side == sideYES {\n\t\treturn \"y\"\n\t}\n\treturn \"n\"\n}\n\n// stakeIdxMine reports whether a key the prefix scan matched is one this code actually\n// wrote for `who`.\n//\n// ONE DEFINITION, TWO CALLERS, and it exists because the second caller was written as a\n// copy of the first: `address` is a bare string type with no length invariant, so a\n// caller-supplied `who` can prefix-match every holder's buckets, and a walk that trusts\n// the match reads other people's state. pruneVoteLocks learned the same lesson worse — it\n// read an unparseable match as permission to DELETE. Two copies of that check is how the\n// second one comes to disagree with the first.\nfunc stakeIdxMine(pre, k string) bool { return len(k) == len(pre)+8 }\n\n// indexPosition records that `who` has opened a position on this claim's side.\n//\n// Called from getPos's CREATION branch only, which fires exactly once per\n// (holder, claim, side) — so entries are unique by construction and this needs no\n// dedup and no read-before-write beyond finding the bucket.\nfunc indexPosition(c *Court, claimID uint64, who address, side int) {\n\tk := stakeIdxKey(who, claimID)\n\tentry := beClaimKey(claimID) + sideByte(side)\n\tif v := c.stakeIdx.Get(k); v != nil {\n\t\t// Appending to the struct's field dirties one object; a tree Set here would\n\t\t// rewrite the leaf. checkpoint.gno's page does the same and says why.\n\t\tr := v.(*stakeRow)\n\t\tr.packed += entry\n\t\treturn\n\t}\n\tc.stakeIdx.Set(k, \u0026stakeRow{packed: entry})\n}\n\n// stakeIdxWalk calls fn for each of this holder's positions: buckets in claim-id order,\n// entries within a bucket in the order they were opened. Returning true stops the walk.\nfunc stakeIdxWalk(c *Court, who address, fn func(claimID uint64, side int) bool) {\n\tif c.stakeIdx == nil {\n\t\treturn\n\t}\n\tpre := string(who)\n\tc.stakeIdx.Iterate(pre, pre+\"\\xff\", func(k string, v any) bool {\n\t\tif !stakeIdxMine(pre, k) {\n\t\t\treturn false\n\t\t}\n\t\tr, ok := v.(*stakeRow)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\tfor i := 0; i+stakeIdxEntry \u003c= len(r.packed); i += stakeIdxEntry {\n\t\t\tside := sideNO\n\t\t\tif r.packed[i+8] == 'y' {\n\t\t\t\tside = sideYES\n\t\t\t}\n\t\t\tif fn(beClaimID(r.packed[i:i+8]), side) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n}\n\n// stakeIdxCount is how many entries a holder has, counted from the packed LENGTHS\n// rather than by visiting entries.\n//\n// The house's *Size readers are O(1) tree Size() calls (strips.gno's StripSize), which\n// this cannot be — c.stakeIdx.Size() counts BUCKETS, not positions. Summing len/9 per\n// bucket is the next best thing and the reason the packed form pays off twice: the cost\n// is O(the holder's own buckets), one object load per 64 claim ids, so a holder with a\n// thousand positions costs sixteen loads rather than a thousand callbacks. The first\n// version of StakedSize walked every entry, which made the one reader that exists to\n// report completeness the one read whose cost grew with the holder's history.\nfunc stakeIdxCount(c *Court, who address) int {\n\tif c.stakeIdx == nil {\n\t\treturn 0\n\t}\n\tn, pre := 0, string(who)\n\tc.stakeIdx.Iterate(pre, pre+\"\\xff\", func(k string, v any) bool {\n\t\tif !stakeIdxMine(pre, k) {\n\t\t\treturn false\n\t\t}\n\t\tif r, ok := v.(*stakeRow); ok {\n\t\t\tn += len(r.packed) / stakeIdxEntry\n\t\t}\n\t\treturn false\n\t})\n\treturn n\n}\n\n// StakedSize is how many positions this holder has ever opened in this court.\n//\n// Positions are never removed — there is no stakers.Remove anywhere in this realm, and\n// the persistence is deliberate: the row IS the participant test, and conviction\n// survives a full unstake so a claim with an unpaid reward must stay findable. So this\n// counts LIFETIME positions, and StakedPage reports each one's live stake beside it so a\n// client can tell an open position from a settled one without a second read.\nfunc StakedSize(courtSlug string, who address) int {\n\tif !who.IsValid() {\n\t\treturn 0\n\t}\n\treturn stakeIdxCount(mustCourt(courtSlug), who)\n}\n\n// StakedPage lists one holder's stake positions in this court, oldest claim\n// first and, within a claim, in the order they were opened:\n//\n//\t\"c:\u003cclaimID\u003e:\u003cside\u003e:\u003cliveStake\u003e:\u003cconvictionCC\u003e;c:…\"\n//\n// side is 0 for YES, 1 for NO. liveStake is what is staked right now, and it is\n// ZERO for a position withdrawn in full — the entry is still listed because its\n// conviction may still be owed, and the last field is that conviction, in CC\n// draw-base units, the same figure ConvictionOf returns.\n//\n// offset and limit page through the list; limit is capped at 200, and a limit of\n// zero or less means that cap. Read StakedSize alongside it to tell a partial\n// page from a complete one.\n//\n// Returns the empty string for an invalid address or for a holder with no\n// positions in the range. Refused only on a court that does not exist.\nfunc StakedPage(courtSlug string, who address, offset, limit int) string {\n\tif !who.IsValid() {\n\t\treturn \"\"\n\t}\n\tif limit \u003c= 0 || limit \u003e stakePageMax {\n\t\tlimit = stakePageMax\n\t}\n\tc := mustCourt(courtSlug)\n\t// The era integral ONCE for the whole page: it is court-level, so computing it per\n\t// position would be the same number N times (stake.gno's ratePreview).\n\tacc := ratePreview(c)\n\tout, seen, sent := \"\", 0, 0\n\tstakeIdxWalk(c, who, func(claimID uint64, side int) bool {\n\t\tif seen \u003c offset {\n\t\t\tseen++\n\t\t\treturn false\n\t\t}\n\t\tseen++\n\t\t// cs.stakers.Get, never getPos — getPos ALLOCATES a position on access, and a\n\t\t// read that persists lets anyone grow the realm for free.\n\t\tvar live, conv int64\n\t\tif cv := c.claims.Get(beClaimKey(claimID)); cv != nil {\n\t\t\tcs := cv.(*claimState)\n\t\t\tif pv := cs.stakers.Get(posKey(who, side)); pv != nil {\n\t\t\t\tpos := pv.(*stakePos)\n\t\t\t\tlive = pos.stake\n\t\t\t\tconv = convPreviewCC(cs, pos, acc)\n\t\t\t}\n\t\t}\n\t\tout += \";c:\" + itoa(int64(claimID)) + \":\" + itoa(int64(side)) +\n\t\t\t\":\" + itoa(live) + \":\" + itoa(conv)\n\t\tsent++\n\t\treturn sent \u003e= limit\n\t})\n\tif out == \"\" {\n\t\treturn \"\"\n\t}\n\treturn out[1:]\n}\n"},{"name":"stakeseries.gno","body":"package kourtv2\n\n// Drawable per-claim stake history (owner ask, 2026-08-17): the two pools as\n// checkpoint series so a chart can show what actually happened, not a shape\n// synthesized from three ratios. Hourly points are kept hot for a week and\n// trimmed past it; daily points are kept for the claim's life. Change-only:\n// an epoch in which the pools did not move stores nothing — that guard is the\n// CALLER's (SetAt archives unconditionally on an epoch roll), the same\n// discipline grc20votes applies at addVotes.\n//\n// This is render/read-layer state: no money path reads any of it.\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\tcheckpoint \"gno.land/p/kourt/checkpoint/v0\"\n)\n\nconst (\n\t// hourlyKeep is how many hourly epochs stay hot — a week, the same figure\n\t// as twapBuckets. Older hourly pages are trimmed; the daily grain answers\n\t// below the horizon.\n\thourlyKeep = uint32(168)\n\t// trimBudget bounds the archive pages freed per trim ride. Production adds\n\t// at most one candidate page per 32 CHANGED hourly epochs per series, so a\n\t// budget of 2 per touched hour strictly outruns it.\n\ttrimBudget = 2\n\t// dailyBlocks is the daily grain's width: 24 hourly epochs. 17,280 blocks\n\t// is already the realm's canonical day (priorityWindowBlocks).\n\tdailyBlocks = int64(17_280)\n\t// seriesRowCap bounds ClaimSeries' output rows (audit N3 discipline: no\n\t// read's cost may grow unbounded with attacker-priced writes). Hourly can\n\t// never reach it (retention 168+2); daily reaches it only on a zombie\n\t// claim staked across ~13 months of daily changes.\n\tseriesRowCap = 400\n)\n\n// csKey names one series' pages in the court's shared archive.\n//\n// NEVER beClaimKey here: it packs raw zero bytes and checkpoint.Sep IS \\x00,\n// so every claim id would panic mustBeUsable. Decimal text is fine — the key\n// is short (≤ 25 bytes against MaxKey 128) and never parsed back.\nfunc csKey(id uint64, suffix string) string {\n\treturn \"cs:\" + strconv.FormatUint(id, 10) + \":\" + suffix\n}\n\n// ensureCSArch lazily creates the court's history archive (write paths only —\n// reads must tolerate nil, per the read-purity rule).\nfunc ensureCSArch(c *Court) *checkpoint.Archive {\n\tif c.csArch == nil {\n\t\tc.csArch = checkpoint.NewArchive()\n\t}\n\treturn c.csArch\n}\n\n// setIfChanged is the no-change ⇒ no-storage guard. SetAt with a rolled epoch\n// archives the previous point even when the value is identical; skipping the\n// write entirely is what keeps a quiet claim at zero bytes.\nfunc setIfChanged(a *checkpoint.Archive, s *checkpoint.Series, key string, e uint32, v int64) {\n\tif v != s.Value() {\n\t\ts.SetAt(a, key, e, v)\n\t}\n}\n\n// recordPools checkpoints both pools on both grains. Contract: runs AFTER the\n// pool mutation (it records the new value), from the same two call sites as\n// observePools — and nowhere else, so a frozen claim's record simply stops.\n// Within one epoch the writes coalesce on the inline slots for free; the trim\n// rides at most once per hourly epoch per claim.\nfunc recordPools(c *Court, cs *claimState) {\n\th := heightNow()\n\ta := ensureCSArch(c)\n\thE := uint32(h/epochBlocks) + 1 // 1-based, the coin's own epoch clock\n\tdE := uint32(h/dailyBlocks) + 1\n\tsetIfChanged(a, \u0026cs.yesH, csKey(cs.id, \"yh\"), hE, cs.yesStake)\n\tsetIfChanged(a, \u0026cs.noH, csKey(cs.id, \"nh\"), hE, cs.noStake)\n\tsetIfChanged(a, \u0026cs.yesD, csKey(cs.id, \"yd\"), dE, cs.yesStake)\n\tsetIfChanged(a, \u0026cs.noD, csKey(cs.id, \"nd\"), dE, cs.noStake)\n\tif hE \u003e cs.histTrimmedTo {\n\t\tcs.histTrimmedTo = hE\n\t\ttrimClaimHistory(c, cs, hE)\n\t}\n}\n\n// trimClaimHistory frees hourly pages older than the keep window, bounded.\n// Also called once at settlement so a finished claim's hourly tail is clipped\n// to its final week without waiting for traffic it will never see again.\nfunc trimClaimHistory(c *Court, cs *claimState, hE uint32) {\n\tif c.csArch == nil || hE \u003c= hourlyKeep+1 {\n\t\treturn\n\t}\n\tkeep := hE - hourlyKeep\n\tc.csArch.Trim(csKey(cs.id, \"yh\"), keep, trimBudget)\n\tc.csArch.Trim(csKey(cs.id, \"nh\"), keep, trimBudget)\n}\n\n// histPoint is one change point during collection.\ntype histPoint struct {\n\te uint32\n\tv int64\n}\n\n// collectSeriesKeyed returns one series' change points ascending — the newest\n// seriesRowCap archived entries plus the two inline points — and whether\n// older archived entries were dropped by the cap. Nil archive is a claim\n// (or court) from before this feature: only the inline points, which for a\n// zero Series is nothing at all.\nfunc collectSeriesKeyed(a *checkpoint.Archive, s *checkpoint.Series, key string) ([]histPoint, bool) {\n\tpts := []histPoint{}\n\tmore := false\n\tif a != nil \u0026\u0026 key != \"\" {\n\t\ta.WalkDesc(key, func(e uint32, v int64) bool {\n\t\t\tif len(pts) \u003e= seriesRowCap {\n\t\t\t\tmore = true\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tpts = append(pts, histPoint{e, v})\n\t\t\treturn false\n\t\t})\n\t}\n\t// Newest-first to ascending.\n\tfor i, j := 0, len(pts)-1; i \u003c j; i, j = i+1, j-1 {\n\t\tpts[i], pts[j] = pts[j], pts[i]\n\t}\n\tif e1, pv := s.Prev(); e1 != 0 {\n\t\tpts = append(pts, histPoint{e1, pv})\n\t}\n\tif e0 := s.Since(); e0 != 0 {\n\t\tpts = append(pts, histPoint{e0, s.Value()})\n\t}\n\treturn pts, more\n}\n\n// ClaimSeries is the recorded history of a claim's two stake pools, one line\n// of text a client parses: \"grain,width,now,more;epoch:yes:no,...\".\n//\n// Rows are CHANGE points, strictly ascending — a missing epoch means the\n// pools did not move, and the consumer forward-fills (charts draw step-after\n// lines, never interpolation). Rows are a contiguous newest-suffix of the\n// change history: between consecutive rows and from the last row to `now`,\n// forward-fill is exact. Epoch e covers heights [(e-1)*width, e*width); a row\n// at e == now is the running epoch's latest value and may still coalesce\n// until the epoch seals. Values are CC base units, the same numbers\n// StakePools serves live; intra-epoch flicker that reverts within an epoch\n// leaves no trace — the record is where the pools settled each epoch, the\n// twap ring's own last-value-in-bucket posture.\n//\n// grain \"hourly\" serves from the trim horizon (~a week) to now; \"daily\"\n// serves the claim's whole life. more=1 means older change points exist but\n// were dropped by the row cap — ask the coarser grain. A 0:0 row is real\n// (everyone unstaked), not absence; empty rows mean the pools never changed.\n//\n// No purge gate, deliberately: the gate exists for TEXT (ClaimTitle's\n// tombstone), and this read carries none — pool sizes are the same numeric\n// surface StakePools already serves ungated (\"stakes, verdict and refunds\n// proceed\", the purge banner's own words). It refuses only a missing\n// court/claim and a bad grain, like every sibling read.\nfunc ClaimSeries(courtSlug string, claimID uint64, grain string) string {\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tvar width int64\n\tvar yS, nS *checkpoint.Series\n\tvar yKey, nKey string\n\tswitch grain {\n\tcase \"hourly\":\n\t\twidth = epochBlocks\n\t\tyS, nS = \u0026cs.yesH, \u0026cs.noH\n\t\tyKey, nKey = csKey(claimID, \"yh\"), csKey(claimID, \"nh\")\n\tcase \"daily\":\n\t\twidth = dailyBlocks\n\t\tyS, nS = \u0026cs.yesD, \u0026cs.noD\n\t\tyKey, nKey = csKey(claimID, \"yd\"), csKey(claimID, \"nd\")\n\tdefault:\n\t\tpanic(`kourtv2: grain is \"hourly\" or \"daily\"`)\n\t}\n\tnow := uint32(heightNow()/width) + 1\n\tys, moreY := collectSeriesKeyed(c.csArch, yS, yKey)\n\tns, moreN := collectSeriesKeyed(c.csArch, nS, nKey)\n\n\t// Union of change epochs, forward-filling the side that did not change.\n\t// When a side's older entries were capped away, rows below its oldest\n\t// KNOWN entry would forward-fill from an unknowable floor — clip the\n\t// union there so every emitted row is exact.\n\tstart := uint32(0)\n\tif moreY \u0026\u0026 len(ys) \u003e 0 \u0026\u0026 ys[0].e \u003e start {\n\t\tstart = ys[0].e\n\t}\n\tif moreN \u0026\u0026 len(ns) \u003e 0 \u0026\u0026 ns[0].e \u003e start {\n\t\tstart = ns[0].e\n\t}\n\tmore := moreY || moreN\n\tcurY, curN := int64(0), int64(0)\n\ti, j := 0, 0\n\tfor i \u003c len(ys) \u0026\u0026 ys[i].e \u003c start {\n\t\tcurY = ys[i].v\n\t\ti++\n\t}\n\tfor j \u003c len(ns) \u0026\u0026 ns[j].e \u003c start {\n\t\tcurN = ns[j].v\n\t\tj++\n\t}\n\trows := []string{}\n\tfor i \u003c len(ys) || j \u003c len(ns) {\n\t\tvar e uint32\n\t\tswitch {\n\t\tcase i \u003e= len(ys):\n\t\t\te = ns[j].e\n\t\tcase j \u003e= len(ns):\n\t\t\te = ys[i].e\n\t\tcase ys[i].e \u003c ns[j].e:\n\t\t\te = ys[i].e\n\t\tdefault:\n\t\t\te = ns[j].e\n\t\t}\n\t\tif i \u003c len(ys) \u0026\u0026 ys[i].e == e {\n\t\t\tcurY = ys[i].v\n\t\t\ti++\n\t\t}\n\t\tif j \u003c len(ns) \u0026\u0026 ns[j].e == e {\n\t\t\tcurN = ns[j].v\n\t\t\tj++\n\t\t}\n\t\trows = append(rows, strconv.FormatUint(uint64(e), 10)+\":\"+\n\t\t\tstrconv.FormatInt(curY, 10)+\":\"+strconv.FormatInt(curN, 10))\n\t}\n\t// The union of two capped sides can itself exceed the cap: keep the\n\t// newest suffix (each row carries absolute values, so any suffix stands\n\t// alone) and say so.\n\tif len(rows) \u003e seriesRowCap {\n\t\trows = rows[len(rows)-seriesRowCap:]\n\t\tmore = true\n\t}\n\tm := \"0\"\n\tif more {\n\t\tm = \"1\"\n\t}\n\treturn grain + \",\" + strconv.FormatInt(width, 10) + \",\" +\n\t\tstrconv.FormatUint(uint64(now), 10) + \",\" + m + \";\" +\n\t\tstrings.Join(rows, \",\")\n}\n"},{"name":"standing.gno","body":"// Standing: the non-transferable participation record that will price posting on\n// a claim's board (docs/CLAIM_BOARDS.md §2). It is NOT the answer record, and the\n// separation is load-bearing — see the two-ledger note below.\n//\n// TWO RULES, AND EVERY CREDIT IN THIS FILE OBEYS BOTH.\n//\n//  1. CONSERVED UNDER SPLITTING. n addresses sharing one holder's weight must\n//     never earn more than that holder would have earned undivided. A per-event\n//     credit (\"you voted: +1\") is sybil-linear for free — a whale becomes twenty\n//     keys, each does a twentieth of the work, and all twenty are paid in full.\n//     So every credit here is PROPORTIONAL to a capital quantity, and the two\n//     that are per-claim (author, dispute) are unsplittable by construction: a\n//     claim has one author, and a decided round has one winner — the answerer it\n//     upheld or the disputer stamped in overturnBy.\n//\n//  2. INHERIT AN EXISTING WEIGHT GATE. Where this realm already built a bar for\n//     \"this actor did the real thing\", the credit hangs off THAT bar and not off\n//     a cheaper cousin one branch over. Three earlier drafts of this file each\n//     picked the cheaper cousin and each was a farm:\n//\n//     - the flag credit on \"conclusive low\" instead of slashGrade. Conclusive\n//     low needs only demotionBar (a QUARTER of the full bar), and on that\n//     branch ResolveFlag returns the flagger's bond WHOLE and mints a bounty\n//     on top — so the credit would have cost negative CC.\n//     - the dispute credit on \"prevailed\" instead of credEligible. That is\n//     verbatim the argument audit M2-1 rejected: OpenDispute's own comment\n//     says the sybil-wallet check is \"hygiene only… the REAL credential\n//     defense is the credEligible weight bar\".\n//     - an authorship credit on MID. Default-mid is what an unlooked-at claim\n//     gets; crediting it is the author-mill's habitat (PLAN §3.4 F1), and it\n//     is the same mistake A18 already corrected for the answer record.\n//\n// WHY STANDING IS DENOMINATED IN CC BASE UNITS. The alternative was a\n// dimensionless point per act, which cannot satisfy rule 1 and cannot be summed\n// across categories that measure different things. Every credit here converts to\n// the CC magnitude of the capital that earned it, scaled by one bps RATE per\n// category (creditRates — a realm default a court may override), so the sum is a\n// single int64 in one unit.\n//\n// satAdd guards it, NOT mustAdd, and the difference is deliberate: these credits\n// run inside settlement, where a checked-overflow panic would strand a verdict.\n// See satAdd's own note.\n//\n// The consequence, stated rather than implied: STANDING IS CAPITAL-WEIGHTED.\n// That is not a defect that survived review, it is the only class of defence\n// MODERATION.md admits — \"address-keyed defenses fall to sybils; only\n// capital-keyed, deadline-keyed, or residency-keyed defenses hold\". An earlier\n// design tried to blunt it with throughput tiers and inverted itself: a tier\n// ladder is concave, so twenty-five small keys out-post one large one, and the\n// ceiling bound the honest single identity while costing the whale nothing.\n//\n// MONOTONE. Nothing in this file reduces a score. Standing does not decay and\n// does not sleep — warmth decays because it is a BAR whose staleness would make\n// a dormant court undecidable, and a posting rate allocates nothing scarce.\n//\n// THAT JUSTIFICATION IS NARROWER THAN IT WAS, and the narrowing is recorded\n// rather than quietly dropped. Once standing also weights an upvote, it buys a\n// position on a page, and a position IS scarce — so monotone-and-permanent means\n// the earliest cohort ranks above later arrivals for good. Two orderings (Top and\n// Newest) bound that to a preference rather than an exclusion, because nothing\n// can be pushed off Newest; it does not erase it. docs/CLAIM_BOARDS.md §2.3\n// carries the full argument and the owner's decision to accept it.\n//\n// The one downward path is a logged m-of-n moderator slash, which lands with the\n// board's moderation lane and not here. highWater is what makes that slash\n// reversible.\n\npackage kourtv2\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"strings\"\n\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\n// The per-category scales, in bps of the capital that earned the credit.\n// CALIBRATION, not structure: the ordering (policing ≥ authorship \u003e staking)\n// is the part that must not move, because it is what stops standing becoming a\n// yield on holding rather than a record of work.\nconst (\n\t// A flag whose slash SETTLED — full bar, two-thirds low, and the answerer's\n\t// counter-window spent without a rescue. Scaled on what the act destroyed\n\t// (dust burns + the slash), which is the same base the bounty is capped\n\t// against. NOT on the bond: on the slashGrade branch the bond comes back\n\t// whole, so \"the bond risked\" — which this comment used to claim — is zero.\n\tstandingFlagBps = int64(10_000)\n\t// A dispute that produced a decided verdict against real opposition. Scaled\n\t// on the POSTED ANSWER BOND (answerBond0), which is the stable magnitude of\n\t// the fight: it is never zeroed as bonds are disposed, and the disputer's own\n\t// bond is min(20%·X̄, 40%×answerBond), i.e. derived from the same number.\n\tstandingDisputeBps = int64(10_000)\n\t// An authored claim the court adjudicated HIGH — two-thirds at the full bar,\n\t// seeded excluded. Scaled on the AUTHOR'S OWN time-averaged stake on the\n\t// winning side.\n\t//\n\t// It reads the author's own capital and not the claim's X̄, which is what it\n\t// read first. X̄ is the crowd's money: keying the row to it made authorship\n\t// the one row whose base the earner did not supply, and it was measured\n\t// minting 2.5 units of standing per CC anyone burned — against a claim whose\n\t// deposit and fee both refund, so the author's own outlay was gas.\n\t//\n\t// TWICE the conviction rate (owner call). Authorship is the strictly harder\n\t// gate — two-thirds at the full bar against no gate at all — so on a shared\n\t// base it must pay more, and standing.gno's stated ordering\n\t// \"policing ≥ authorship \u003e staking\" is only true if it does. At the previous\n\t// 500 against conviction's 1,000 the ordering inverted the moment the bases\n\t// were unified, which is why the two changes land together.\n\tstandingAuthorHighBps = int64(2_000)\n\t// Winning conviction, linear and uncapped, on the staker's own TIME-AVERAGED\n\t// STAKE. Linear is the unique splitting-neutral rule (PLAN §3.5 F8): any\n\t// clamp or per-claim cap is concave, and a concave credit pays a split\n\t// position more than a whole one — the defect the carrot's P2 per-voter\n\t// clamp note in openrewards.gno records.\n\t//\n\t// It reads time-averaged stake and not convCC, which is what it read first.\n\t// convCC is a FLOW — stake x rate x time — and the other three rows are\n\t// stocks, so the ratio between this row and its neighbours was a function of\n\t// how long the claim happened to run: measured at ~15,000x out of proportion\n\t// on a short claim, ~50x at two weeks, ~8x at twelve. No single constant can\n\t// calibrate a row whose units differ from the ones it is ordered against.\n\tstandingConvictionBps = int64(1_000)\n\t// What an UNDISPUTED claim's winners earn, as a fraction of the adjudicated\n\t// rate. Half (owner call).\n\t//\n\t// The first version of this gate paid them nothing, on the reasoning that a\n\t// default mid is what a claim NOBODY LOOKED AT receives. That reasoning is\n\t// sound about the mill and wrong about everyone else, because it makes the\n\t// credit turn on whether somebody chose to dispute you — and a dispute is\n\t// partly noise. Anyone may open one to grief. Requiring one before a correct\n\t// position counts punishes a staker for not having been attacked, and since\n\t// most claims settle undisputed BY DESIGN it would have made the row pay\n\t// almost never.\n\t//\n\t// So an undisputed stand is worth something, and worth less: a quality vote\n\t// that actually concluded is the stronger evidence and pays double.\n\t//\n\t// WHAT PRICES THE MILL HERE IS THE FLAG LANE, not this gate. Anyone may flag\n\t// a junk claim; a conclusive low zeroes the tier, and the tier arm above then\n\t// pays its winners nothing at all. That is the same lane the money side\n\t// already leans on for the same attacker, which is why standing does not need\n\t// a second, stricter gate of its own — and it is why the residual is a\n\t// flag-probability question (ECONOMICS.md's q) rather than a new one.\n\tstandingUnadjudicatedBps = int64(5_000)\n)\n\n// ---- the credit rates: a realm default a court may override -------------\n//\n// NAMED creditRates AND NOT standingRates, because check-nontransferable.py\n// refused the latter and was right to. Its verb census forbids\n// `Set…Standing…(cur realm` precisely so that \"an entrypoint called\n// AssignRecord, MigrateCredential, SetStanding, Bequeath or MoveScore\" cannot\n// sail through while doing the one thing that must never be possible. These set\n// what a credit is WORTH, never anyone's score — and a name that reads like the\n// second is a name worth changing rather than a guard worth exempting.\n//\n// The five constants above are the LAUNCH CALIBRATION and the realm-wide\n// default; a court's moderators may override them for their own court. Same two\n// places, same reasoning, as association.gno's bond: the realm default so a new\n// court is never unpriced, the override so a court need not wait on a realm\n// upgrade to reprice its own speech layer.\n//\n// WHY THESE MAY MOVE WHEN THE EMISSION CONSTANTS MAY NOT. PLAN.md §0 forbids\n// runtime knobs and court.gno says the absence of a retune path is load-bearing\n// for every overflow and capture bound in the audit — and that is about the\n// TOKENOMICS constants, which those bounds rest on. Standing is not money: no\n// audit bound depends on these, no arithmetic overflow turns on them, and\n// standing is per-court, so a court that sets them badly harms its own board and\n// nobody else's. That is COURTS_TOKENOMICS §9a's test — \"could a founder who set\n// this maliciously harm someone who did not read the charter?\" — answered no.\n//\n// SET AS A TUPLE, NEVER ONE AT A TIME, and this is the whole reason for the\n// struct. The bound that matters is not a range on any single number, it is the\n// ORDERING policing \u003e= authorship \u003e staking, which this file calls the part that\n// must not move. Five independent setters would leave a window between two calls\n// where the ordering is violated and credits are being paid through it. One\n// setter, one check, no window.\ntype creditRates struct {\n\tflag, dispute, authorHigh, conviction, unadjudicated int64\n}\n\nvar creditRatesDefault = creditRates{\n\tflag:          standingFlagBps,\n\tdispute:       standingDisputeBps,\n\tauthorHigh:    standingAuthorHighBps,\n\tconviction:    standingConvictionBps,\n\tunadjudicated: standingUnadjudicatedBps,\n}\n\n// creditRatesFor resolves the two places. A nil override means inherit — not\n// \"all zero\", which for a five-tuple would be indistinguishable from a court\n// that deliberately zeroed its own board.\nfunc creditRatesFor(c *Court) creditRates {\n\tif c.creditRates != nil {\n\t\treturn *c.creditRates\n\t}\n\treturn creditRatesDefault\n}\n\n// mustCreditRates refuses a set that breaks the ordering or leaves the range\n// scaleBps can honour.\n//\n// THE ORDERING IS CHECKED ON RATES, NOT ON REALIZED CREDITS, and the two can\n// differ — measured, on a launch court an overturner earns 1.2 CC-equiv against\n// an author's 2.0, so policing lands BELOW authorship in practice while the bps\n// still satisfy policing \u003e= authorship. That is not a defect in the check: the\n// rows read different bases (a party's own bond against a staker's own\n// time-averaged stake), and how big anyone's bond is relative to anyone's stake\n// is their choice, not a settable parameter. The rate ordering is the only form\n// of the rule a setter can enforce. Read the file header's ordering claim as\n// being about the rates, because that is what is guarded.\n//\n// The upper bound is a REFUSAL rather than a clamp on purpose: scaleBps clamps\n// at 10_000 too, but silently, so a court that set 12_000 would get 10_000 and\n// no indication its intent had been discarded. A setter should say no.\nfunc mustCreditRates(r creditRates) {\n\tfor _, v := range []int64{r.flag, r.dispute, r.authorHigh, r.conviction, r.unadjudicated} {\n\t\tif v \u003c 0 || v \u003e 10_000 {\n\t\t\tpanic(\"kourtv2: a standing rate is 0..10000 bps\")\n\t\t}\n\t}\n\tif r.flag \u003c r.authorHigh || r.dispute \u003c r.authorHigh {\n\t\tpanic(\"kourtv2: policing must credit at least as much as authorship\")\n\t}\n\tif r.authorHigh \u003c= r.conviction {\n\t\tpanic(\"kourtv2: authorship must credit more than staking — it is the harder gate\")\n\t}\n\tif r.unadjudicated \u003c= 0 || r.unadjudicated \u003e= 10_000 {\n\t\tpanic(\"kourtv2: the unadjudicated rate must be a real fraction of the adjudicated one\")\n\t}\n}\n\n// SetCreditRatesDefault sets the realm-wide default every court inherits.\nfunc SetCreditRatesDefault(cur realm, flag, dispute, authorHigh, conviction, unadjudicated int64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\td := ensureGlobalDAO()\n\tif cur.Previous().Address() != d.admin {\n\t\tpanic(\"kourtv2: only the global DAO admin sets the default standing rates\")\n\t}\n\tr := creditRates{flag, dispute, authorHigh, conviction, unadjudicated}\n\tmustCreditRates(r)\n\tcreditRatesDefault = r\n\t// See SetLadderDefault: realm-wide, so the event is the only record.\n\tchain.Emit(globalActEvent,\n\t\t\"court\", \"*\",\n\t\t\"claim\", \"0\",\n\t\t\"act\", \"set-rates-default:\"+creditRatesKey(r),\n\t\t\"by\", cur.Previous().Address().String(),\n\t\t\"height\", eventHeight(),\n\t)\n}\n\n// SetCourtCreditRates overrides the default for one court.\nfunc SetCourtCreditRates(cur realm, courtSlug string, flag, dispute, authorHigh, conviction, unadjudicated int64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\t// An ACTIVE moderator, as repricing association is: setting what work is\n\t// worth is not one of the de-escalatory acts a suspended set may still take.\n\tcm := ensureMod(c)\n\trequireActiveMod(cm, who)\n\tr := creditRates{flag, dispute, authorHigh, conviction, unadjudicated}\n\tmustCreditRates(r)\n\t// speechM-of-n, logged, and clearable by the global DAO, for the reason\n\t// SetCourtLadder's note gives at length: this shipped as a single-member,\n\t// unlogged, permanent lever. SetCourtCreditRates(slug, 2, 2, 2, 1, 1) passes\n\t// mustCreditRates — the ordering holds — and cuts every earning rate by\n\t// ~5000x, so nobody in the court ever climbs off level 0 again. The values go\n\t// in the action key so two members cannot approve different tuples into one\n\t// threshold.\n\tif fire, _ := approveAction(cm.pending, \"setrates:\"+creditRatesKey(r), who, \"\",\n\t\tspeechM(cm)); !fire {\n\t\treturn\n\t}\n\tc.creditRates = \u0026r\n\tcm.appendCourtLog(who, \"setrates:\"+creditRatesKey(r), \"\")\n\temitModAct(c.id, 0, \"setrates\", who)\n}\n\n// creditRatesKey is the tuple as one string — the thing voted on and the thing\n// logged, from one definition.\nfunc creditRatesKey(r creditRates) string {\n\treturn strconv.FormatInt(r.flag, 10) + \":\" + strconv.FormatInt(r.dispute, 10) +\n\t\t\":\" + strconv.FormatInt(r.authorHigh, 10) + \":\" +\n\t\tstrconv.FormatInt(r.conviction, 10) + \":\" +\n\t\tstrconv.FormatInt(r.unadjudicated, 10)\n}\n\n// ClearCourtCreditRates returns a court to the realm default.\nfunc ClearCourtCreditRates(cur realm, courtSlug string) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tcm := ensureMod(c)\n\t// requireMod: returning to the realm default is de-escalation, so it must\n\t// survive a suspension. See ClearCourtLadder.\n\trequireMod(cm, who)\n\tif c.creditRates == nil {\n\t\tpanic(\"kourtv2: this court has no standing-rate override\")\n\t}\n\tif fire, _ := approveAction(cm.pending, \"clearrates:\"+c.id, who, \"\",\n\t\tspeechM(cm)); !fire {\n\t\treturn\n\t}\n\tc.creditRates = nil\n\tcm.appendCourtLog(who, \"clearrates\", \"\")\n\temitModAct(c.id, 0, \"clearrates\", who)\n}\n\n// StandingRates is what this court currently pays, packed:\n//\n//\tflag:\u003cn\u003e;dispute:\u003cn\u003e;author:\u003cn\u003e;conviction:\u003cn\u003e;unadjudicated:\u003cn\u003e;inherited:\u003c0|1\u003e\nfunc CreditRates(courtSlug string) string {\n\tc := mustCourt(courtSlug)\n\tr := creditRatesFor(c)\n\tinherited := \"1\"\n\tif c.creditRates != nil {\n\t\tinherited = \"0\"\n\t}\n\treturn \"flag:\" + strconv.FormatInt(r.flag, 10) +\n\t\t\";dispute:\" + strconv.FormatInt(r.dispute, 10) +\n\t\t\";author:\" + strconv.FormatInt(r.authorHigh, 10) +\n\t\t\";conviction:\" + strconv.FormatInt(r.conviction, 10) +\n\t\t\";unadjudicated:\" + strconv.FormatInt(r.unadjudicated, 10) +\n\t\t\";inherited:\" + inherited\n}\n\n// The four categories, as an ORDERED LIST rather than four named constants and\n// four named fields.\n//\n// The shape is what stops a whole class of defect. An earlier version validated\n// the category in one switch and applied it in a second, and nothing made the\n// two agree: add a fifth category to the first and forget the second, and the\n// score rises with no subtotal — the breakdown silently stops summing, which is\n// the one property the page rests on, and no test would see it. Indexing one\n// array through one lookup makes the divergence unrepresentable.\n//\n// The strings stay wire tokens the page and the overlay parse, so the ORDER of\n// this list is part of that wire and adding to the middle would renumber every\n// stored row. Append only.\nvar standingCats = [standingCatN]string{\"flag\", \"dispute\", \"author\", \"conviction\"}\n\nconst (\n\tstandingCatFlag = iota\n\tstandingCatDispute\n\tstandingCatAuthor\n\tstandingCatConv\n\tstandingCatN\n)\n\n// standingRow is one address's participation record in one court.\n//\n// The four subtotals are not decoration and not a log. They exist because the\n// page has to be able to say HOW a score was earned — a single opaque number\n// that gates what you may say is exactly the shape nobody can argue with, and\n// \"learn how to earn it, then see where yours came from\" is the whole point of\n// the page. Four int64s is the cheapest form of that; a per-event log would be\n// unbounded state on a surface an attacker can drive.\ntype standingRow struct {\n\tscore int64 // the sum; monotone in this file\n\n\t// highWater is the largest score this address has ever held, and it is what\n\t// makes a slash REVERSIBLE — which is the property the whole authority model\n\t// now rests on, because there is no appeals lane. A successor moderator set's\n\t// remedy for a bad slash is to raise `score` back toward this number, and\n\t// without it a successor has no way to know what to put back, so \"restore\"\n\t// would be a mint rather than a repair.\n\t//\n\t// It rises ONLY here, from the earning table, and never from a moderator act\n\t// — that asymmetry is the whole of its safety. A slash lowers `score` and\n\t// leaves this untouched; a restore raises `score` and may never pass it.\n\t// The invariant is `score \u003c= highWater`, always, and it holds by\n\t// construction rather than by check: the one place score rises is the one\n\t// place this rises with it.\n\thighWater int64\n\n\t// passHeld is set by BuyCommentPass. It floors the posting level at 1\n\t// (posting.gno) and is NOT standing — buying it credits no score, or the\n\t// record would be purchasable.\n\t//\n\t// ONE THING CLEARS IT: a slash all the way to level 0 (boardmod.gno). The\n\t// burn is never refunded — the entitlement is forfeit, not reversed. This\n\t// field was documented as never-cleared, and that made the slash defeatable\n\t// for 0.1 CC: an address slashed to zero standing still posted at entry rate\n\t// forever, leaving only the 12h freeze against a persistent abuser.\n\tpassHeld bool\n\n\t// The posting bucket (posting.gno). Here rather than in a second tree\n\t// because every post reads the level, which reads this row already.\n\ttokens     int64\n\tlastRefill int64 // unix seconds; 0 = never posted\n\n\t// The board freeze (boardmod.gno): the height it lifts, and the court\n\t// sanction epoch it was stamped under. A freeze is live only while the epoch\n\t// still matches, which is what lets one write clear every freeze in a court.\n\t// Expiry is LAZY — nothing sweeps, exactly as every other clock here.\n\t//\n\t// It bars BOARD WRITES ONLY. A frozen address still stakes, votes, answers,\n\t// disputes, flags, opens claims, withdraws, draws emission and takes every\n\t// election action; the election is the remedy for a capturing set, so a\n\t// freeze that reached any of those could silence the vote that removes it.\n\tfrozenUntil     int64\n\tfrozenUntilTime int64 // unix seconds beside it; 0 on freezes predating the stamps\n\tfrozenEpoch     int64\n\n\t// frozenGapUntil is the height before which this address may not be frozen\n\t// AGAIN, and it is deliberately a separate stamp from frozenUntil.\n\t// frozenGapUntilTime is its wall-clock twin, written at the same moment.\n\t//\n\t// The gap used to be computed from frozenUntil, and two things defeated it.\n\t// UnfreezeBoard sets frozenUntil = 0, so freeze -\u003e unfreeze -\u003e freeze slipped\n\t// the gap for the price of one extra speechM round, letting a set hold an\n\t// address frozen indefinitely in 12h increments — the exact outcome\n\t// freezeMaxBlocks exists to prevent. And the gap test also required a\n\t// matching sanction epoch, so GlobalClearBoardBits — a RECOVERY verb — handed\n\t// the set an immediate re-freeze.\n\t//\n\t// This stamp is therefore cleared by neither: not by the undo, because\n\t// de-escalating must not buy the set a fresh window, and not by the epoch,\n\t// because the backstop exists to protect the address rather than to re-arm\n\t// the power against it.\n\tfrozenGapUntil     int64\n\tfrozenGapUntilTime int64 // unix seconds beside it; 0 on gaps predating the stamps\n\n\t// from[i] is what category standingCats[i] contributed. An ARRAY, not four\n\t// fields and not a slice: fixed width persists inline and cannot be nil, so\n\t// a row read back always has four subtotals that sum to the score.\n\tfrom [standingCatN]int64\n}\n\n// getStanding is the ALLOCATING accessor: write paths only. It is named in\n// check-read-purity.py's ALLOCATORS for that reason — a read that reached for it\n// would let the first caller change what the second one sees, and a query\n// carries no storage deposit, so it would be unpaid state growth.\n//\n// Tolerates a nil tree for the same reason lockedOf does: a fixture that built a\n// Court by hand has one, and a defensive branch here is cheaper than a fixture\n// sweep. startCourt creates it, so the branch is dead on every real court.\nfunc getStanding(c *Court, who address) *standingRow {\n\tif c.standings == nil {\n\t\tc.standings = bptree.NewBPTree32()\n\t}\n\tif v := c.standings.Get(string(who)); v != nil {\n\t\treturn v.(*standingRow)\n\t}\n\tr := \u0026standingRow{}\n\tc.standings.Set(string(who), r)\n\treturn r\n}\n\n// lookupStanding is the NON-allocating twin, and the only one a read may call.\n// The split mirrors getRecord/AnswerRecord in records.gno; keeping it explicit\n// here rather than reusing one function is what makes the read path checkable.\nfunc lookupStanding(c *Court, who address) *standingRow {\n\tif c.standings == nil {\n\t\treturn nil\n\t}\n\tif v := c.standings.Get(string(who)); v != nil {\n\t\treturn v.(*standingRow)\n\t}\n\treturn nil\n}\n\n// creditStanding adds to a score and to exactly one subtotal.\n//\n// ZERO AND NEGATIVE ARE NO-OPS, not panics, and that is deliberate: every caller\n// sits on a money path at a lifecycle transition, and MODERATION.md's rule for\n// calls in that direction is that they must never be able to abort one. A claim\n// with no conviction on the winning side, a seeded claim, a court whose\n// arithmetic floors to nothing — each reaches here with a zero and must leave\n// the transition untouched.\n//\n// VALIDATE, THEN ALLOCATE, THEN ADD — the order is the whole correctness of this\n// function, and it is written this way because an earlier version was written\n// the other way round. That one added to the score first and undid the add in a\n// switch's default arm with `r.score -= amount`, which failed twice at the\n// edges: satAdd is the one operation here that is NOT invertible, so at the\n// ceiling the add saturated and the subtraction then took back the full amount,\n// leaving the score BELOW where it started — a silent monotonicity break on the\n// exact input satAdd exists to survive; and it called getStanding first, so a\n// miscategorised credit persisted a row for an address that ended up with\n// nothing.\n//\n// Nothing to undo means nothing to get wrong. The category is now an INDEX\n// rather than a string, so \"miscategorised\" is a range check rather than a\n// second switch that could drift out of step with the first.\nfunc creditStanding(c *Court, who address, amount int64, cat int) {\n\tif amount \u003c= 0 || who == \"\" || cat \u003c 0 || cat \u003e= standingCatN {\n\t\treturn\n\t}\n\tr := getStanding(c, who)\n\tr.score = satAdd(r.score, amount)\n\tr.from[cat] = satAdd(r.from[cat], amount)\n\t// The high-water mark tracks the MAX, not the latest: after a slash, score\n\t// sits below it and a later credit must climb back rather than reset the\n\t// ceiling to wherever the score happens to have landed.\n\tif r.score \u003e r.highWater {\n\t\tr.highWater = r.score\n\t}\n}\n\n// satAdd SATURATES where every pool total in this realm uses the checked\n// mustAdd, and the difference is the whole reason this function exists.\n//\n// mustAdd panics, and a panic here would land inside WithdrawBonus, OpenRewards,\n// Finalize or settleSlash — the four money paths this file's credits actually\n// hang off, each of them a lifecycle transition. MODERATION.md's rule\n// for calls in that direction is absolute: a moderation-side invariant must\n// never be able to block a lifecycle transition. So an unreachable arithmetic\n// edge must not be able to strand a settlement, and this is the same call econ\n// vet F4 already made about the per-claim draw: \"checked-abort on a shared path\n// is a settlement DoS — clamp instead.\"\n//\n// Saturating is safe here in a way it would NOT be for a pool total, because\n// this number is a record rather than a claim on anything: a saturated score\n// over-states standing at the ceiling and over-states nothing else. No coin, no\n// draw and no vote is derived from it. Reaching the ceiling would need roughly\n// ten thousand maximum-supply claims, so it is unreachable — the point is what\n// happens if it is ever reached, not how likely that is.\nfunc satAdd(a, b int64) int64 {\n\tconst maxInt64 = int64(9223372036854775807)\n\tif b \u003e 0 \u0026\u0026 a \u003e maxInt64-b {\n\t\treturn maxInt64\n\t}\n\treturn a + b\n}\n\n// scaleBps is the one multiply every credit goes through, on the audited 128-bit\n// path rather than mustMul for the reason satAdd saturates: at the top of the\n// range a CC magnitude times 10,000 bps exceeds int64, and mustMul's answer to\n// that is a panic on a settlement path. mulDiv128 carries the product in 128\n// bits, so the multiply cannot overflow at any supply this realm can reach.\n//\n// THE BPS CLAMP is not defensive noise: mulDiv128's surviving panic arm is\n// `q \u003e MaxInt64`, and the only thing that makes it unreachable here is\n// bps \u003c= 10_000, which bounds q by base. The block above is labelled\n// CALIBRATION — i.e. those constants are expected to move — so a bump to 12_000\n// would arm a panic inside Finalize and settleSlash, the exact outcome\n// satAdd and this function exist to prevent. Clamping keeps the guarantee\n// unconditional instead of resting it on a constant someone may retune.\nfunc scaleBps(base, bps int64) int64 {\n\tif base \u003c= 0 || bps \u003c= 0 {\n\t\treturn 0\n\t}\n\tif bps \u003e 10_000 {\n\t\tbps = 10_000\n\t}\n\treturn mulDiv128(base, bps, 10_000)\n}\n\n// ------------------------------------------------------------- the credits --\n\n// creditFlagSlash pays the beneficiary of a slash that actually SETTLED.\n//\n// CALLED FROM settleSlash, never from ResolveFlag. Three things were wrong with\n// paying it at the slashGrade branch instead, and each is a separate rule:\n//\n//   - THE ACT WAS NOT FINAL. ResolveFlag only reserves; ResolveCounter can still\n//     rescue the slash. A credit paid at reservation survived a repudiation the\n//     money lane reverses, leaving the flagger paid for a flag a larger,\n//     full-bar electorate threw out.\n//   - THE BENEFICIARY WAS WRONG. cs.flagger can be the zero address on a\n//     ride-levied slash, or a stale flagger from an earlier cycle. slashFlagger\n//     is recorded at origination for exactly this reason.\n//   - THE BASE WAS PUMPABLE. Scaling on the posted bond was doubly wrong: on the\n//     slashGrade branch the bond is returned WHOLE (the half-burn arm requires\n//     !slashGrade), so nothing was risked, and flagBondFor multiplied by\n//     flagCycles, which an inconclusive-LOW advanced while returning the bond\n//     whole — two free cycles, four times the credit. (flagBondFor and\n//     flagCycles went with the quality lane; this paragraph is kept because the\n//     REASON the base is what it is outlives the mechanism that forced it, but\n//     nothing here is a function you can still go and read.)\n//\n// So the base was what the act DESTROYED, which is also what the bounty was\n// capped against.\n//\n// THE FUNCTION IS GONE, and with it the only production path that ever paid a\n// flag credit. Its single caller was settleSlash, which went with the quality\n// lane; verified before removing — `creditFlagSlash` appeared nowhere in the\n// realm but its own declaration.\n//\n// TWO THINGS OUTLIVE IT AND ARE NOT TIDY. standingCatFlag stays, because a\n// standing record's per-category counters are part of StandingOf's exported\n// string and dropping one changes a shape readers parse. And the `flag` RATE\n// stays settable through SetCreditRatesDefault and SetCourtCreditRates, is\n// reported by CreditRates and AdminParams, and is still constrained by\n// mustCreditRates below — so an administrator can be REFUSED for setting\n// authorship above a rate that now credits nobody. Removing it means changing\n// two exported signatures and a packed output shape, which is an owner's call\n// rather than a cleanup; it is filed, and until then the parameters page says\n// the rate is inert instead of implying it pays.\n\n// creditDisputeResolved pays whichever party a DECIDED round vindicated.\n//\n// It rides the same predicate the answer record already uses (dispute.gno), and\n// deliberately so: an uphold earns only when it was credEligible — i.e. the\n// overturn side carried at least a quarter of the credential weight floor —\n// because otherwise ~20% of a dispute bond buys standing on any trivially-true\n// claim. An OVERTURN needs no such gate: it had to clear quorum and the verdict\n// threshold to happen at all, so it is weight-gated by construction.\n// NEVER cs.disputer. That field is the CURRENT round's disputer and is\n// overwritten by every OpenDispute, while a failed reopen leaves an earlier\n// overturn's `provisional` standing — so reading it here paid whoever opened the\n// last round, measured as a profitable theft that also denied the honest\n// overturner. cs.overturnBy is stamped at the round that won.\n// THE decidedRounds GUARD IS REDUNDANT TODAY, AND IS KEPT ANYWAY. Verified\n// against every writer rather than assumed: `cs.provisional` differs from\n// `cs.answer` only where dispute.gno's OVERTURN branch sets it, two lines above\n// its own `decidedRounds++`; and `cs.credEligible` is written in exactly one\n// place, inside the UPHELD branch, likewise above `decidedRounds++`. So both\n// arms below already imply a decided round and this cannot change behaviour.\n//\n// It stays because the redundancy lives in ANOTHER FILE: it holds only while\n// those two fields keep their current writers, and nothing enforces that from\n// here. It is recorded as a permanent mutation gap rather than papered over —\n// no test can kill it, and a corpus row that always survives is one people\n// learn to skim past.\nfunc creditDisputeResolved(c *Court, cs *claimState) {\n\tif cs.decidedRounds \u003c= 0 {\n\t\treturn\n\t}\n\t// EACH SIDE ON ITS OWN BOND. answerBond0 IS the answerer's own posted\n\t// magnitude, but the disputer's is min(20%·X̄, 40%·answerBond0) and the\n\t// answer arm always binds, so paying both on answerBond0 gave the disputer\n\t// 2.5x the capital they actually risked.\n\tif cs.provisional != cs.answer {\n\t\tcreditStanding(c, cs.overturnBy, scaleBps(cs.overturnBond, creditRatesFor(c).dispute), standingCatDispute)\n\t\treturn\n\t}\n\tif cs.credEligible {\n\t\tcreditStanding(c, cs.answerer, scaleBps(cs.answerBond0, creditRatesFor(c).dispute), standingCatDispute)\n\t}\n}\n\n\n// creditWinConviction pays a winning staker in proportion to the conviction they\n// actually drew on — linear, uncapped, and taken from the same convCC the draw\n// itself is computed from, so the two can never disagree about what was held.\n//\n// TWO RATES, NOT A GATE. The rate turns on whether the COURT decided this claim\n// or a deadline did; see standingUnadjudicatedBps and `adjudicated` below.\n//\n// THE TIER ARM IS GONE WITH THE TIER, and what it did is worth keeping on the\n// record because it is the deterrent this row no longer has. It refused a claim\n// the court had adjudicated LOW: tier 0 multiplied the winners' slice to\n// nothing, and before the arm existed every winning staker was still credited in\n// full anyway. Measured against the mill's total money punishment on that branch\n// — the deposit and the fee, and nothing else — the standing take was 4.5× the\n// burn on a young court and 22.7× on a mature one. So the flag lane's entire\n// deterrent reached the money and left the record untouched, which was A18's\n// lesson unapplied to the newer ledger.\n//\n// With no tier there is no LOW to refuse, so every winning staker is credited\n// and the adjudicated rate is the only thing that still varies. The 4.5×/22.7×\n// figures above are what that costs on the mill branch, and they are the reason\n// this paragraph is a record rather than a deletion.\n//\n// provClose needs no arm of its own, and the arm it once had was wrong.\n// open the rewards says it plainly — \"provClose now DRAWS. Only a dead claim has no\n// draw\" — so an earlier comment here, that provClose has \"no draw and no\n// verdict\", was false in every clause and withheld a credit on a path that pays\n// MID emission. It resolves without being named: a provClose is rated like any\n// other claim on the adjudicated rate.\n//\n// THAT SENTENCE USED TO SAY \"carries whatever tier and slotConsumed it ended up\n// with, and is rated on those\", which contradicted the function immediately\n// below it — creditWinConviction opens \"The tier arm is gone with the tier.\"\n// slotConsumed is not declared anywhere any more either. The conclusion held;\n// only its stated reason had stopped being the reason.\nfunc creditWinConviction(c *Court, cs *claimState, who address, p *stakePos) {\n\t// The tier arm is gone with the tier. It refused a conclusive LOW, which\n\t// drew nothing — \"the record follows the money\" — and with no tier there is\n\t// no low to refuse. Every winning staker is credited; the adjudicated rate\n\t// below is the only thing that still varies.\n\tr := creditRatesFor(c)\n\tbps := r.conviction\n\tif !adjudicated(cs) {\n\t\tbps = bps * r.unadjudicated / 10_000\n\t}\n\tcreditStanding(c, who, scaleBps(timeAvgStake(cs, p.rawHi, p.rawLo), bps), standingCatConv)\n}\n\n// adjudicated reports whether the COURT decided this claim, rather than a\n// deadline deciding it by nobody turning up.\n//\n// ONE DEFINITION, and the reason is the same one qualityQuestionOpen exists for:\n// a sentence this load-bearing spelled two ways is how two callers come to mean\n// different things by it. check-epoch-coherence counts that pattern.\n//\n// IT WAS `!cs.slotConsumed`, which was set in exactly two places, both of them a\n// quality tally that reached a conclusion. That field is going away with the\n// quality lane, and the surviving thing that means \"the court actually decided\n// something here\" is a dispute round that reached a verdict — decidedPID is set\n// at dispute.gno's two decide sites and nowhere else.\n//\n// WHAT MOVES, stated rather than discovered later: a claim whose quality was\n// adjudicated by a FLAG but which was never disputed used to earn the full\n// conviction rate and now earns half. That population is exactly the one the\n// flag lane existed to reach, and it is being removed on purpose. Nothing else\n// changes hands — a dispute-decided claim was full-rate before and is full-rate\n// now, and an untouched claim was half before and is half now.\nfunc adjudicated(cs *claimState) bool { return cs.decidedPID != 0 }\n\n// ------------------------------------------------------------------ reads --\n\n// Standing is an address's participation score in one court, in CC base units.\n//\n// It is NOT the answer record, and a page showing one as the other would be\n// wrong in a way nobody could see: AnswerRecord is adjudicated and no moderator\n// can change it, while this number is slashable for abuse. Quote both or say\n// which one this is.\nfunc Standing(courtSlug string, who address) int64 {\n\tif r := lookupStanding(mustCourt(courtSlug), who); r != nil {\n\t\treturn r.score\n\t}\n\treturn 0\n}\n\n// StandingBreakdown is the score and its four subtotals, packed:\n//\n//\tscore:\u003cn\u003e;flag:\u003cn\u003e;dispute:\u003cn\u003e;author:\u003cn\u003e;conviction:\u003cn\u003e\n//\n// Packed rather than five reads for the reason ClaimSeries and\n// ClaimAssociations are: one round trip, and a shape the overlay already parses.\n// Every field is always present, so a client never has to distinguish \"absent\"\n// from \"zero\" — an address that has never participated reads as all zeroes\n// rather than as an error, because having no standing is a normal state and not\n// a missing record.\nfunc StandingBreakdown(courtSlug string, who address) string {\n\tvar row standingRow\n\tif r := lookupStanding(mustCourt(courtSlug), who); r != nil {\n\t\trow = *r\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"score:\" + strconv.FormatInt(row.score, 10))\n\tb.WriteString(\";highwater:\" + strconv.FormatInt(row.highWater, 10))\n\tfor i, name := range standingCats {\n\t\tb.WriteString(\";\" + name + \":\" + strconv.FormatInt(row.from[i], 10))\n\t}\n\treturn b.String()\n}\n"},{"name":"strips.gno","body":"// The policing surfaces (MODERATION.md §5.3) — the load-bearing guarantee of\n// the whole moderation design.\n//\n// Hiding a claim is render-layer, but it still shapes ECONOMICS, because V2's\n// junk defense (the quality flag lane) is discovery-driven: a claim nobody\n// discovers in time is a claim nobody flags, and it settles at the paying MID\n// default. So a hidden claim must never fall off EVERY surface while it is\n// still policeable. Two indexed strips (per-court and realm-wide) plus a\n// per-court pending list carry that guarantee, and they IGNORE the courtMod and\n// meta hide bits by construction.\n//\n// Ordering is the anti-flood spine: rows sort by their actionable DEADLINE\n// (answerHeight + settleDelay), nearest-to-mint first. A mill cannot manufacture\n// a past deadline, and it cannot keep its own about-to-settle target off the top\n// of the page — the target is discoverable-for-policing for its whole settle\n// window. Keys are big-endian so ascending iteration IS deadline order; entry\n// and exit compute the same key from immutable fields, so membership is\n// idempotent and needs no stored key.\n//\n// Every write here is called BY the money path (PostAnswer, OpenRewards,\n// provCloseClaim, CloseDeadClaim) and therefore must never panic: a\n// moderation-side invariant may not block a lifecycle transition (§2).\n\npackage kourtv2\n\nimport bptree \"gno.land/p/nt/bptree/v0\"\n\n// stripActorCap bounds how many rows one answerer or one author may occupy on a\n// contended page. It binds ONLY when the index overflows a page — an\n// uncontended strip renders every row (round-4 F3b). Caps key the FREE\n// dimensions (addresses), so they only blunt a sybil blockade; the deadline\n// ordering is what defeats it. Seeded rows are exempt: a seed-farm burying\n// itself past the cap is covered by the \"seeded\" badge, so capping them would\n// only hide the evidence (round-4 code-truth F4).\nconst stripActorCap = 5\n\n// stripRow identifies one policeable claim on the realm-wide directory strip.\ntype stripRow struct {\n\tslug    string\n\tclaimID uint64\n}\n\n// globalStripIdx is the realm-wide policing index behind the directory strip:\n// be(deadline)|slug|be(claimID) -\u003e *stripRow. It exists so a TIER-HIDDEN\n// court's policeable claims are still discoverable (round-2 M-A14: court-level\n// hiding was the same turnout lever one level up).\nvar globalStripIdx = bptree.NewBPTree32()\n\n// beHeightKey encodes a height big-endian so string order is numeric order.\nfunc beHeightKey(h int64) string {\n\tif h \u003c 0 {\n\t\th = 0\n\t}\n\treturn beClaimKey(uint64(h))\n}\n\n// stripDeadline is the row's ordering key: the settle deadline on the dominant\n// (undisputed) path. Deliberately computed from the IMMUTABLE answer height\n// rather than tracking the live actionable height — a stable key never needs\n// re-keying, and §5.3 only claims the ordering APPROXIMATES urgency once a\n// claim leaves the dominant path (flag/escrow/counter windows restamp\n// open the rewards-eligibility, never this key).\nfunc stripDeadline(cs *claimState) int64 { return cs.answerHeight + settleDelay }\n\nfunc stripKey(cs *claimState) string {\n\treturn beHeightKey(stripDeadline(cs)) + beClaimKey(cs.id)\n}\n\nfunc globalStripKey(slug string, cs *claimState) string {\n\treturn beHeightKey(stripDeadline(cs)) + slug + beClaimKey(cs.id)\n}\n\n// pendingKey orders the pending list by open height. Honest note (round-5 F2):\n// a pre-answer row has no SettleDeadline, so the pending list CANNOT carry the\n// deadline spine, and this key sorts a flood earliest. Its guarantee is\n// pagination + the priced door, and the real escape is that an appellant can\n// self-answer to move their row onto the deadline-ordered strip.\nfunc pendingKey(cs *claimState) string {\n\treturn beHeightKey(cs.openedAt) + beClaimKey(cs.id)\n}\n\n// ---- membership writes (called by the money path; never panic) ------------\n\n// stripEnter puts an answered claim on both policing strips. Idempotent.\nfunc stripEnter(c *Court, cs *claimState) {\n\tif cs.inStrip {\n\t\treturn\n\t}\n\tif c.stripIdx == nil {\n\t\tc.stripIdx = bptree.NewBPTree32()\n\t}\n\tc.stripIdx.Set(stripKey(cs), cs.id)\n\tglobalStripIdx.Set(globalStripKey(c.id, cs), \u0026stripRow{slug: c.id, claimID: cs.id})\n\tcs.inStrip = true\n}\n\n// stripExit removes a claim once it is no longer policeable: rewardsOpened,\n// provClosed, or dead-closed. Idempotent.\nfunc stripExit(c *Court, cs *claimState) {\n\tif !cs.inStrip {\n\t\treturn\n\t}\n\tif c.stripIdx != nil {\n\t\tc.stripIdx.Remove(stripKey(cs))\n\t}\n\tglobalStripIdx.Remove(globalStripKey(c.id, cs))\n\tcs.inStrip = false\n}\n\n// pendingEnter puts an unanswered seeded claim (or, once the meta module lands,\n// an unanswered `mod:` appeal) on the court's pending list, so a pre-answer row\n// can never be embargoed off every surface. Idempotent.\nfunc pendingEnter(c *Court, cs *claimState) {\n\tif cs.inPending {\n\t\treturn\n\t}\n\tif c.pendingIdx == nil {\n\t\tc.pendingIdx = bptree.NewBPTree32()\n\t}\n\tc.pendingIdx.Set(pendingKey(cs), cs.id)\n\tcs.inPending = true\n}\n\n// pendingExit drops a pending row: it moved to the strip at PostAnswer, or it\n// dead-closed. Idempotent.\nfunc pendingExit(c *Court, cs *claimState) {\n\tif !cs.inPending {\n\t\treturn\n\t}\n\tif c.pendingIdx != nil {\n\t\tc.pendingIdx.Remove(pendingKey(cs))\n\t}\n\tcs.inPending = false\n}\n\n// ---- reads (render + clients; never read by a money path) -----------------\n\n// stripPageIDs walks a court's strip in nearest-deadline order and returns up\n// to limit claim IDs after skipping offset rows, applying the per-actor cap\n// only when the index overflows one page.\nfunc stripPageIDs(c *Court, offset, limit int) []uint64 {\n\tout := []uint64{}\n\tif c.stripIdx == nil || limit \u003c= 0 {\n\t\treturn out\n\t}\n\tcapBinds := c.stripIdx.Size() \u003e renderPageSize\n\tperAnswerer := map[string]int{}\n\tperAuthor := map[string]int{}\n\tskipped := 0\n\tc.stripIdx.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tid, ok := v.(uint64)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\tcv := c.claims.Get(beClaimKey(id))\n\t\tif cv == nil {\n\t\t\treturn false\n\t\t}\n\t\tcs := cv.(*claimState)\n\t\tif capBinds \u0026\u0026 !cs.seeded {\n\t\t\ta := cs.answerer.String()\n\t\t\tb := cs.author.String()\n\t\t\tif perAnswerer[a] \u003e= stripActorCap || perAuthor[b] \u003e= stripActorCap {\n\t\t\t\treturn false // this actor already holds its share of a contended page\n\t\t\t}\n\t\t\tperAnswerer[a]++\n\t\t\tperAuthor[b]++\n\t\t}\n\t\tif skipped \u003c offset {\n\t\t\tskipped++\n\t\t\treturn false\n\t\t}\n\t\tout = append(out, id)\n\t\treturn len(out) \u003e= limit\n\t})\n\treturn out\n}\n\n// StripPage is the per-court policing strip: the claims that are answered but\n// not yet rewardsOpened/provClosed/closed, nearest-deadline first, regardless of\n// any courtMod or meta hide bit. Paginated so the guarantee never bottoms out\n// at one page (§4).\nfunc StripPage(courtSlug string, offset, limit int) []uint64 {\n\treturn stripPageIDs(mustCourt(courtSlug), offset, limit)\n}\n\n// StripSize is how many policeable rows a court currently has.\nfunc StripSize(courtSlug string) int {\n\tc := mustCourt(courtSlug)\n\tif c.stripIdx == nil {\n\t\treturn 0\n\t}\n\treturn c.stripIdx.Size()\n}\n\n// PendingPage is the per-court pending list (unanswered seeded claims and, once\n// the meta module lands, unanswered `mod:` appeals), oldest first.\nfunc PendingPage(courtSlug string, offset, limit int) []uint64 {\n\tout := []uint64{}\n\tc := mustCourt(courtSlug)\n\tif c.pendingIdx == nil || limit \u003c= 0 {\n\t\treturn out\n\t}\n\t// IterateByOffset descends to the offset in O(log n) instead of stepping\n\t// over it. The strip page below cannot do this — its per-actor cap counts\n\t// across the whole prefix ON PURPOSE, so that a deep page stays consistent\n\t// with the pages before it — but the pending list has no such term, and a\n\t// caller-supplied offset is otherwise a linear walk they pay gas for.\n\tc.pendingIdx.IterateByOffset(offset, limit, func(_ string, v any) bool {\n\t\tif id, ok := v.(uint64); ok {\n\t\t\tout = append(out, id)\n\t\t}\n\t\treturn false\n\t})\n\treturn out\n}\n\n// PendingSize is how many pending rows a court currently has.\nfunc PendingSize(courtSlug string) int {\n\tc := mustCourt(courtSlug)\n\tif c.pendingIdx == nil {\n\t\treturn 0\n\t}\n\treturn c.pendingIdx.Size()\n}\n\n// DirectoryStripPage is the realm-wide policing strip, nearest-deadline first\n// across every court — including tier-hidden ones. Returned as parallel slices\n// so a caller gets (slug, claimID) pairs without a /p/ pointer escaping realm\n// state (borrow rule #2). A per-court sub-cap keeps one court from occupying\n// the page when it overflows.\nfunc DirectoryStripPage(offset, limit int) ([]string, []uint64) {\n\tslugs := []string{}\n\tids := []uint64{}\n\tif limit \u003c= 0 {\n\t\treturn slugs, ids\n\t}\n\tcapBinds := globalStripIdx.Size() \u003e renderPageSize\n\tperCourt := map[string]int{}\n\tskipped := 0\n\tglobalStripIdx.Iterate(\"\", \"\", func(_ string, v any) bool {\n\t\tr, ok := v.(*stripRow)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\tif capBinds {\n\t\t\tif perCourt[r.slug] \u003e= stripActorCap {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tperCourt[r.slug]++\n\t\t}\n\t\tif skipped \u003c offset {\n\t\t\tskipped++\n\t\t\treturn false\n\t\t}\n\t\tslugs = append(slugs, r.slug)\n\t\tids = append(ids, r.claimID)\n\t\treturn len(slugs) \u003e= limit\n\t})\n\treturn slugs, ids\n}\n\n// DirectoryStripSize is the realm-wide policeable row count.\nfunc DirectoryStripSize() int { return globalStripIdx.Size() }\n\n// OnStrip / OnPending are the membership reads (render badges; tests).\nfunc OnStrip(courtSlug string, claimID uint64) bool {\n\treturn mustClaim(mustCourt(courtSlug), claimID).inStrip\n}\n\nfunc OnPending(courtSlug string, claimID uint64) bool {\n\treturn mustClaim(mustCourt(courtSlug), claimID).inPending\n}\n"},{"name":"supersede.gno","body":"// Supersedes edges: a claim re-filed after the question it asks died unanswered.\n// docs/SUPERSEDES.md is the design, the settled authorization rule, and the\n// argument against that rule which was kept rather than buried.\n//\n// WHY THIS IS NOT A SECOND COPY OF association.gno. An argument edge is speech —\n// \"this claim supports that one\" is a view, anybody may add one, and the chain\n// checks nothing because there is nothing to check. This is a statement about\n// the docket's own history, and the chain holds the evidence: claimState.closed\n// is written in exactly one place, only when a claim has no answer and the\n// dead-claim timeout has passed. So the predicate is verified rather than taken\n// on trust.\n//\n// AND WHY VERIFYING IT DOES NOT MAKE IT PERMISSIONLESS, which is the part that\n// took three reviews to settle. The realm's genuinely permissionless\n// entrypoints — CloseDeadClaim, ResolveDispute — have predicates LOGICALLY\n// EQUIVALENT to their act: if the guard passes the write is correct whoever\n// sent it, so \"the verdict is the authority, not the caller\". This predicate is\n// necessary and not sufficient. It certifies that the pair is ELIGIBLE, not that\n// the two claims ask the same question — and since dead claims are produced\n// constantly, nearly every one of them is an eligible `to` for any given `from`.\n// The caller picks one out of hundreds, and that choice is the whole assertion.\n\npackage kourtv2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\tbptree \"gno.land/p/nt/bptree/v0\"\n)\n\nconst (\n\t// maxSupIn bounds the read: a claim page lists who re-filed it.\n\tmaxSupIn = 8\n\t// THE ANTI-SYBIL TERM, and the audit line it repairs. An earlier draft argued\n\t// no per-author cap was needed because \"the flood is priced by the claim, not\n\t// the edge\" — eight inbound slots costing eight real deposits. That is true\n\t// only because assertion is author-only, which was the very rule still under\n\t// question when it was written; and even then one address can fill all eight\n\t// with eight of its own claims. Argument edges kept a per-author term with an\n\t// inbound cap eight times looser, for the same reason.\n\tmaxSupInPerAuthor = 2\n)\n\n// supEdge is ONE object in TWO indexes, the shape association.gno established. `to`\n// is redundant with supBy's key and necessary in supOf's, which is keyed by\n// `from` alone because outbound is capped at one.\ntype supEdge struct {\n\tto     uint64\n\tauthor address\n\tat     int64 // height, for the record; nothing gates on it\n}\n\n// ensureSup allocates the two indexes. WRITE PATHS ONLY — named in\n// check-read-purity.py's ALLOCATORS, because a read that allocated would let the\n// first caller change what the second one sees, unpaid.\nfunc ensureSup(c *Court) {\n\tif c.supOf == nil {\n\t\tc.supOf = bptree.NewBPTree32()\n\t}\n\tif c.supBy == nil {\n\t\tc.supBy = bptree.NewBPTree32()\n\t}\n}\n\n// supersedeOrdered reports whether `from` was filed no earlier than the moment\n// `to` could first have died.\n//\n// The wall clock is preferred and the height is the fallback, which is exactly\n// how CloseDeadClaim decides the same question about the same claim — a claim\n// opened before the clock was running carries openedAtTime 0, and a predicate\n// that only knew about stamps would refuse every such claim for ever.\n//\n// This is also what makes cycles UNREPRESENTABLE rather than refused: every edge\n// moves at least twelve weeks forward, so a chain of re-filings cannot close on\n// itself and no cycle walk is needed. An edit that weakens this check would take\n// that property away silently.\nfunc supersedeOrdered(from, to *claimState) bool {\n\tif from.openedAtTime != 0 \u0026\u0026 to.openedAtTime != 0 {\n\t\treturn from.openedAtTime \u003e= to.openedAtTime+deadClaimSecs\n\t}\n\treturn from.openedAt \u003e= to.openedAt+deadClaimTimeout\n}\n\n// SupersedeClaim records that `from` re-files `to`, which died unanswered.\n//\n// The author of `from`, or an active court moderator. Narrower than an argument\n// edge deliberately: outbound is capped at ONE and removal belongs to the edge's\n// author, so under a permissionless rule a stranger could take a claim's only\n// outgoing slot with a `to` its author would never have chosen — an assertion\n// about what they meant by filing, which they could not remove, permanently\n// blocking the true edge, for one storage deposit. AddAssociation has no such\n// exposure because its cardinality is 32 and non-exclusive.\nfunc SupersedeClaim(cur realm, courtSlug string, from, to uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tif from == to {\n\t\tpanic(\"kourtv2: a claim cannot supersede itself\")\n\t}\n\t// Both endpoints in THIS court: a cross-court edge would split one object's\n\t// moderation between two moderator sets, the rule associations took.\n\tf := mustClaim(c, from)\n\tt := mustClaim(c, to)\n\n\tif f.author != who {\n\t\t// ensureMod, NOT `c.mod == nil`. A court's moderation state is allocated\n\t\t// lazily and ensureMod seeds the CREATOR as its first member, so a nil\n\t\t// c.mod does not mean \"this court has no moderators\" — it means nobody has\n\t\t// moderated yet. Reading it as the former refuses the one person who is\n\t\t// certainly a moderator, on exactly the courts where nothing has happened.\n\t\t//\n\t\t// Membership spelled out so the refusal names both parties who may act.\n\t\tcm := ensureMod(c)\n\t\tif !cm.members.Has(who.String()) {\n\t\t\tpanic(\"kourtv2: only the re-filing claim's author or a court moderator may record it\")\n\t\t}\n\t\trequireActiveMod(cm, who)\n\t}\n\tif !t.closed {\n\t\tpanic(\"kourtv2: only a claim that died unanswered can be superseded\")\n\t}\n\tif !supersedeOrdered(f, t) {\n\t\tpanic(\"kourtv2: a re-filing must come after the claim it re-files could have died\")\n\t}\n\n\tensureSup(c)\n\tif c.supOf.Get(beClaimKey(from)) != nil {\n\t\tpanic(\"kourtv2: that claim already re-files one; remove that edge first\")\n\t}\n\n\tinN, mine := 0, 0\n\tp := beClaimKey(to)\n\tc.supBy.Iterate(p, \"\", func(key string, v any) bool {\n\t\tif !strings.HasPrefix(key, p) {\n\t\t\treturn true\n\t\t}\n\t\tinN++\n\t\tif e, ok := v.(*supEdge); ok \u0026\u0026 e.author == who {\n\t\t\tmine++\n\t\t}\n\t\treturn false\n\t})\n\tif inN \u003e= maxSupIn {\n\t\tpanic(\"kourtv2: that claim has already been re-filed \" + strconv.Itoa(maxSupIn) + \" times\")\n\t}\n\tif mine \u003e= maxSupInPerAuthor {\n\t\tpanic(\"kourtv2: you already record \" + strconv.Itoa(maxSupInPerAuthor) +\n\t\t\t\" re-filings of that claim\")\n\t}\n\n\te := \u0026supEdge{to: to, author: who, at: heightNow()}\n\tc.supOf.Set(beClaimKey(from), e)\n\tc.supBy.Set(beClaimKey(to)+beClaimKey(from), e)\n}\n\n// RemoveSupersede withdraws the edge `from` asserts. Its author, or an active\n// moderator — the composite RemoveAssociation uses, and reversible by re-adding,\n// which is why it is single-signer.\nfunc RemoveSupersede(cur realm, courtSlug string, from uint64) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\twho := cur.Previous().Address()\n\tc := mustCourt(courtSlug)\n\tif c.supOf == nil {\n\t\tpanic(\"kourtv2: that claim re-files nothing\")\n\t}\n\tv := c.supOf.Get(beClaimKey(from))\n\tif v == nil {\n\t\tpanic(\"kourtv2: that claim re-files nothing\")\n\t}\n\te := v.(*supEdge)\n\trequireEdgeRemover(c, e.author, who)\n\t// BOTH indexes, or the edge survives in one direction and the two reads\n\t// disagree about whether it exists.\n\tc.supOf.Remove(beClaimKey(from))\n\tc.supBy.Remove(beClaimKey(e.to) + beClaimKey(from))\n}\n\n// ClaimSupersedes is every re-filing edge touching one claim, packed:\n//\n//\tof:\u003cid\u003e;by:\u003cid\u003e,\u003cid\u003e\n//\n// `of` is the one claim this one re-files (empty if none); `by` is every claim\n// that re-files THIS one. Both halves always present, either may be empty\n// (\"of:;by:\"). Nine rows at worst, against ClaimAssociations' ninety-six.\n//\n// Reads nil trees as empty and never allocates: a court that has never carried\n// an edge answers without the query creating anything.\nfunc ClaimSupersedes(courtSlug string, claimID uint64) string {\n\tc := mustCourt(courtSlug)\n\tmustClaim(c, claimID)\n\tvar b strings.Builder\n\tb.WriteString(\"of:\")\n\tif c.supOf != nil {\n\t\tif v := c.supOf.Get(beClaimKey(claimID)); v != nil {\n\t\t\te := v.(*supEdge)\n\t\t\t// The same gate the titles take: an edge pointing at a purged or\n\t\t\t// globally-redacted claim is not returned, or the read would put a\n\t\t\t// withheld claim's id back on another claim's page.\n\t\t\tif !assocTextGone(c, e.to) {\n\t\t\t\tb.WriteString(strconv.FormatUint(e.to, 10))\n\t\t\t}\n\t\t}\n\t}\n\tb.WriteString(\";by:\")\n\tif c.supBy != nil {\n\t\tp := beClaimKey(claimID)\n\t\tn := 0\n\t\tc.supBy.Iterate(p, \"\", func(key string, v any) bool {\n\t\t\tif !strings.HasPrefix(key, p) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tother := claimIDFromKey(key[8:])\n\t\t\tif assocTextGone(c, other) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif n \u003e 0 {\n\t\t\t\tb.WriteString(\",\")\n\t\t\t}\n\t\t\tb.WriteString(strconv.FormatUint(other, 10))\n\t\t\tn++\n\t\t\treturn false\n\t\t})\n\t}\n\treturn b.String()\n}\n"},{"name":"supplyseries.gno","body":"package kourtv2\n\n// The two court-wide histories the court page charts: coin supply, and the\n// GNOT burned to mint it.\n//\n// SUPPLY COSTS NOTHING TO KEEP. The ledger already checkpoints it on every\n// mint and its archive is never trimmed, so that half is pure formatting.\n//\n// BURN DOES, and it is one row per epoch in which somebody bought. It cannot be\n// derived from supply: supply also moves on reward emissions, which burn no\n// GNOT, and the burn total counts bond and deposit burns that never minted\n// anything. Two different questions, so two different records.\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Names for the two court-wide histories in the court's shared archive. Not\n// cs: keys — those are per-claim and these are the whole court.\nconst (\n\tburnSeriesKey  = \"burn\"\n\tpriceSeriesKey = \"price\"\n)\n\n// recordPrice checkpoints the coin price. Called from the two places the curve\n// position moves — Buy and the meta franchise mint — after it moves.\n//\n// THE PRICE, NOT THE POSITION. Price is position/d in whole units, so it only\n// ticks when the position crosses a multiple of d. Recording the price is\n// therefore strictly cheaper than recording the position: a court can take many\n// buys and store no row, because change-only means no change is no write.\n//\n// Must not panic: Buy is the only GNOT on-ramp, and an index that degrades\n// beats buying that bricks.\nfunc recordPrice(c *Court) {\n\tsetIfChanged(ensureCSArch(c), \u0026c.priceH, priceSeriesKey,\n\t\tuint32(heightNow()/epochBlocks)+1, c.crv.Price(c.minted))\n}\n\n// recordBurn checkpoints the court's burn total. Called from reindexBurn, after\n// the total moves, and nowhere else.\n//\n// Must not panic: reindexBurn runs inside Buy, the only GNOT on-ramp, and an\n// index that degrades is always preferable to buying that bricks. setIfChanged\n// writes a fixed key and an int64, neither of which has a failing case.\nfunc recordBurn(c *Court) {\n\tsetIfChanged(ensureCSArch(c), \u0026c.burnH, burnSeriesKey,\n\t\tuint32(heightNow()/epochBlocks)+1, c.burnedGNOT)\n}\n\n// formatSeries renders change points as \"width,now,more;epoch:value,...\".\n//\n// ONE FORMATTER FOR BOTH READS. The grammar is a contract with the chart, and\n// two copies of it are two things that can drift apart — the client would parse\n// one correctly and the other silently as garbage.\nfunc formatSeries(width int64, now uint32, pts []histPoint, more bool) string {\n\trows := make([]string, 0, len(pts))\n\tfor _, p := range pts {\n\t\trows = append(rows, strconv.FormatUint(uint64(p.e), 10)+\":\"+\n\t\t\tstrconv.FormatInt(p.v, 10))\n\t}\n\tm := \"0\"\n\tif more {\n\t\tm = \"1\"\n\t}\n\treturn strconv.FormatInt(width, 10) + \",\" +\n\t\tstrconv.FormatUint(uint64(now), 10) + \",\" + m + \";\" +\n\t\tstrings.Join(rows, \",\")\n}\n\n// SupplySeries is the court's coin supply history, one line of text a client\n// parses:\n//\n//\t\"width,now,more;epoch:supply,...\"\n//\n// Rows are CHANGE points, strictly ascending. A missing epoch means supply did\n// not move, so a chart forward-fills and draws step-after lines, never\n// interpolation. Epoch e covers heights [(e-1)*width, e*width), and a row at\n// e == now is the running epoch, which may still coalesce until it seals.\n// Values are CC base units, the same numbers CoinSupply serves live.\n//\n// more=1 means older points were dropped by the row cap. The rows kept are the\n// newest, and each carries an absolute value, so any suffix stands alone.\n//\n// Empty rows mean the supply never moved, not that the read failed.\nfunc SupplySeries(courtSlug string) string {\n\tc := mustCourt(courtSlug)\n\n\tpts := []histPoint{}\n\tmore := false\n\tc.coin.WalkSupply(func(e uint32, v int64) bool {\n\t\tif len(pts) \u003e= seriesRowCap {\n\t\t\tmore = true\n\t\t\treturn true\n\t\t}\n\t\tpts = append(pts, histPoint{e, v})\n\t\treturn false\n\t})\n\t// The walk is newest-first; rows are ascending.\n\tfor i, j := 0, len(pts)-1; i \u003c j; i, j = i+1, j-1 {\n\t\tpts[i], pts[j] = pts[j], pts[i]\n\t}\n\n\treturn formatSeries(c.coin.EpochBlocks(), c.coin.Epoch(), pts, more)\n}\n\n// BurnSeries is the cumulative GNOT burned into this court's curve over time,\n// in the same grammar as SupplySeries. Values are ugnot and never decrease —\n// the curve is one way, so this is the real value the court has absorbed.\n//\n// Read it as the court's livelihood: supply can rise on emissions nobody paid\n// for, but this only moves when somebody spent.\nfunc BurnSeries(courtSlug string) string {\n\tc := mustCourt(courtSlug)\n\tpts, more := collectSeriesKeyed(c.csArch, \u0026c.burnH, burnSeriesKey)\n\treturn formatSeries(epochBlocks, uint32(heightNow()/epochBlocks)+1, pts, more)\n}\n\n// PriceSeries is the coin price over time, in ugnot per unit, in the same\n// grammar as the other two. It never falls: the curve is one way and the\n// position is monotone.\n//\n// Rows are sparser than buys. Price is the position divided by the curve's\n// width, so a run of small buys that does not cross a boundary leaves no row —\n// the price genuinely did not change.\n//\n// A young court has NO rows, because its price is still zero and zero is where\n// the series already sits. Read the stretch before the first row as zero, the\n// same forward-fill the other two series ask for, run backwards.\nfunc PriceSeries(courtSlug string) string {\n\tc := mustCourt(courtSlug)\n\tpts, more := collectSeriesKeyed(c.csArch, \u0026c.priceH, priceSeriesKey)\n\treturn formatSeries(epochBlocks, uint32(heightNow()/epochBlocks)+1, pts, more)\n}\n"},{"name":"testclock.gno","body":"package kourtv2\n\n// The test clock: a one-shot latch that lets a THROWAWAY chain walk claims\n// through deadlines measured in days, and that a chain anybody has already\n// used can never open (owner ruling, 2026-08-18; hardened after an audit).\n//\n// Why it lives in the realm at all. Published deadlines gate on wall-clock time\n// (clock.gno), which is the right unit for a promise to a reader — but it makes\n// end-to-end testing impossible on a real node, because no test can wait 72\n// hours, and the integration harness has no way to skew a chain's clock. There\n// is no dev-only build of this realm to hide such a knob in. So the knob is\n// here, and its safety is structural rather than conventional.\n//\n// WHAT THE LATCH ACTUALLY PROMISES — stated exactly, because the first draft of\n// this comment overclaimed and an audit called it out. It does NOT say \"a real\n// deployment can never open this\". It says: only the address that deployed the\n// package, and only while the realm is PRISTINE — no user court, and the meta\n// court seated by init carrying no claims and no coin. In practice that is the\n// deploying transaction's own window. A deployer who arms their own production\n// chain in that window has armed it; no code can prevent an operator from\n// deciding to run a test chain.\n//\n//  1. init() records the deployer. MsgAddPackage sets OriginCaller, and init is\n//     the only moment that address can be read without a later caller\n//     influencing it (meta.gno relies on the same fact to seat the meta court).\n//  2. Arming requires that address, on a pristine realm, once.\n//  3. Only the deployer may advance or seal. An armed chain is a test chain,\n//     and it stays one — creating courts does NOT disarm it, because seeding a\n//     court and then advancing time is the entire point.\n//  4. Sealing is one-way and never rewinds: the clock floor is kept, so a\n//     stamp already written can never end up in the future.\n//\n// FROZEN, NOT SKEWED. While armed the clock reads base+advanced, where base was\n// captured at arming — real time stops passing. A skew added to a live clock\n// cannot express a boundary: an integration test that advances to one second\n// short of a deadline still crosses it, because the seconds a node spends\n// mining the intervening blocks are added underneath. That was found by the\n// first integration test written against this feature, not in review.\n//\n// What it deliberately cannot do: rewind (advance is forward-only and both the\n// step and the total are capped, so int64 cannot wrap into a negative — an audit\n// proved two maximal \"forward\" steps otherwise walked the clock backwards, and\n// capping the advance turned out to be only half of it, since a declared BASE\n// near the ceiling wraps the same sums; see maxBase); touch heights (conviction,\n// emission and voting epochs are denominated in blocks and must stay honest); or\n// hide (TestClockActive is public AND read by Render and ClaimTimeline, so a\n// reader is never shown a fabricated date without being told).\n\nimport (\n\t\"time\"\n\n\truntime \"chain/runtime\"\n\n\tunsafe \"chain/runtime/unsafe\"\n)\n\nconst (\n\t// maxAdvanceStep bounds one step, maxAdvanceTotal the sum. Ten and a\n\t// hundred years: far past any deadline this realm has (the longest is the\n\t// 12-week dead-claim timeout), and far short of the int64 wrap that made\n\t// two \"forward\" steps move the clock backwards.\n\tmaxAdvanceStep  = int64(10 * 365 * 86400)\n\tmaxAdvanceTotal = int64(100 * 365 * 86400)\n\t// The height caps, in blocks. SIZED AGAINST THE REALM, not against int64 —\n\t// which is the mistake the first version made, and an audit proved what it\n\t// costs. `touch()` walks emission periods ONE AT A TIME (emission.gno), so\n\t// the gas of the next transaction into a court grows with the periods\n\t// elapsed since it was last touched. Measured: ~520k gas steady state,\n\t// 12,519,425 after one legal step at the old 10M cap (82 periods), and\n\t// 713,771,568 at the old 1e9 total (8,267 periods). Past the per-transaction\n\t// gas ceiling the touch reverts, `lastAccrual` is never written, the next\n\t// attempt costs the same — and EVERY entrypoint that touches first is dead\n\t// in that court for ever, sealing included, because the floor holds the\n\t// height where it is. A single legal call, irreversible.\n\t//\n\t// So the total is bounded by what a scenario legitimately needs: the widest\n\t// height-denominated journey in the realm is three failed dispute rounds,\n\t// 3 x (votingBlocks + graceBlocks) = 414,720 blocks, under 3.5 periods. Ten\n\t// periods leaves room and bounds the worst-case catch-up to ten rolls.\n\t// Two periods a step, because ONE is not enough for a real gate: closing a\n\t// dispute is votingBlocks + graceBlocks = 138,240 blocks, which a one-period\n\t// step (120,960) refuses. Caught by the dispute scenario, not by review.\n\tmaxHeightStep  = 2 * periodBlocks  // ~2 weeks, covers the widest single gate\n\tmaxHeightTotal = 10 * periodBlocks // ~10 weeks; see above\n\t// maxBase is the latest instant a scenario may DECLARE. A base of any size\n\t// used to be accepted, and one near int64's ceiling wraps tcBase+tcSkew\n\t// NEGATIVE on an advance that was individually legal — tcNow's non-positive\n\t// assertion then fires on EVERY read for ever after, so the realm stops\n\t// answering (every deadline, every Render, every State) instead of the\n\t// mistake being refused in the transaction that made it.\n\t//\n\t// THIS IS A PLAUSIBILITY CAP, AND THE FIRST VERSION WAS AN ARITHMETIC ONE:\n\t// maxInt64 - maxAdvanceTotal, sized so tcNow's own sum could not wrap. That\n\t// was the same mistake one level out. Every CONSUMER of tcNow adds again —\n\t// deadlineTime and pastDeadline compute stamp+secs, a reopen stamps\n\t// nowTime()+blocksToSecs(...) — so at an arithmetic cap's boundary tcNow\n\t// returns exactly maxInt64 and THOSE wrap: a claim is born past its 12-week\n\t// timeout, unanswerable, closeable by anyone, publishing a negative settle\n\t// stamp. A loud brick traded for silent mis-gating, which is worse.\n\t//\n\t// A real date removes the class rather than the two additions I could\n\t// enumerate: a base below 2100 plus the century of advance leaves every\n\t// downstream sum nine orders of magnitude clear of the ceiling, whatever\n\t// windows get added later. It also refuses the operator error an arithmetic\n\t// cap arms happily — a base in MILLISECONDS, which wraps nothing and simply\n\t// publishes dates in the year 57438.\n\t//\n\t// And arithmetic alone could not have been made sound here in any case: the\n\t// downstream addend is UNBOUNDED. escrowMaxBlocks is a court parameter checked\n\t// only for max \u003e= min \u003e= 1 (court.gno), and blocksToSecs multiplies by five, so\n\t// no headroom computed from today's window constants stays correct.\n\tmaxBase = int64(4102444800) // 2100-01-01T00:00:00Z\n)\n\nvar (\n\ttcDeployer address // captured at deploy; the only address that may drive it\n\ttcArmed    bool    // the clock is frozen+advanced and every surface says so\n\ttcSealed   bool    // one-way: once true, arming is impossible forever\n\ttcSkew     int64   // seconds advanced by the scenario; only ever grows\n\ttcBase     int64   // block time captured at arming; the clock FREEZES here\n\ttcFloor    int64   // the highest instant this clock ever showed; never rewound past\n\t// tcEverArmed outlives sealing. Disarming stops the clock being DRIVEN, but\n\t// the stamps a scenario already wrote stay fabricated forever, so the\n\t// disclosure has to outlive the capability. Sealing used to clear every flag\n\t// and leave a chain of invented dates presenting itself as an honest one.\n\ttcEverArmed bool\n\ttcPeakSkew  int64 // the furthest the clock was ever advanced; kept past sealing\n\n\t// HEIGHT. Wall-clock alone was not enough: conviction accrues per block,\n\t// emission rolls per period, votes close on a block and the twap ring\n\t// matures on one, so a chain that could only fast-forward its CALENDAR\n\t// still had to mine its way to every accounting state — ~17,280 blocks to\n\t// open the rewards, 120,960 to resolve a dispute. The owner reversed the earlier\n\t// \"accounting clocks must not be faked\" ruling for exactly that reason.\n\t//\n\t// Same shape as the time skew, deliberately: forward-only, capped so the\n\t// sum cannot wrap, deployer-only, and floored so a sealed chain never\n\t// reports a height below one it already published.\n\ttcHeightSkew int64\n\ttcHeightPeak int64 // survives sealing, like tcPeakSkew, for the disclosure\n\t// tcHeightBase FREEZES the height at arming, exactly as tcBase freezes the\n\t// clock. The first version of this half was a LIVE skew — real height plus\n\t// the advance — which reintroduced, for height, the precise defect the\n\t// \"FROZEN, NOT SKEWED\" note above records for time. An audit PROVED it: with\n\t// a live skew the floor is written from a height that keeps climbing, so on\n\t// sealing the realm reports a height BELOW ones already written into claims,\n\t// and both p/twap and p/checkpoint correctly refuse a backwards clock —\n\t// bricking every Stake and every Transfer in that court for ever.\n\ttcHeightBase  int64\n\ttcHeightFloor int64\n)\n\nfunc init() { tcDeployer = unsafe.OriginCaller() }\n\n// mustDeployer is the single gate on every write here. Ordered deployer-first\n// so a stranger learns nothing about the latch's state (an audit noted the\n// reverse order was free reconnaissance for exactly the race this defends).\nfunc mustDeployer(cur realm) {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\tif cur.Previous().Address() != tcDeployer {\n\t\tpanic(\"kourtv2: only the deployer may drive the test clock\")\n\t}\n}\n\n// EnableTestClock arms the clock. Deployer only, once, and only while nothing\n// has happened yet: no user court, and the meta court seated by init still\n// carrying no claims and no coin. A realm with history is not a test chain.\nfunc EnableTestClock(cur realm) {\n\tmustDeployer(cur)\n\tif tcSealed {\n\t\tpanic(\"kourtv2: the test clock is sealed; this realm is not a test chain\")\n\t}\n\tif tcArmed {\n\t\tpanic(\"kourtv2: the test clock is already armed\")\n\t}\n\tif CourtCount() \u003e 1 {\n\t\tpanic(\"kourtv2: this realm already has courts; the test clock cannot be armed\")\n\t}\n\t// The meta court exists from init and is a fully working court — it takes\n\t// coin and claims. Court COUNT alone would call a realm with months of\n\t// activity on it \"virgin\", which an audit demonstrated.\n\tm := mustCourt(metaSlug)\n\tif m.nextID \u003e 1 || m.coin.TotalSupply() \u003e 0 {\n\t\tpanic(\"kourtv2: this realm has been used; the test clock cannot be armed\")\n\t}\n\ttcArmed = true\n\ttcEverArmed = true         // never cleared again, not even by sealing\n\ttcBase = time.Now().Unix() // the clock stops here until the scenario moves it\n\t// The cap applies to the CAPTURED base too, or the invariant tcBase \u003c= maxBase\n\t// holds at only one of this field's two writers. Block time is the proposer's\n\t// (clock.gno says so), and the chains this latch targets are hand-rolled\n\t// single-operator ones, so a genesis_time in milliseconds is a likelier way to\n\t// arrive here than clock drift — and it publishes year-57438 dates without\n\t// wrapping anything.\n\t//\n\t// NOT MEASURABLE, and deliberately not billed for: no harness reaches this\n\t// line. The unit suite stops at the court gate above (an earlier test has\n\t// always seated one), a filetest's caller is never the deployer, and a txtar\n\t// cannot set a chain's genesis time. A mutation row for it would survive for\n\t// ever, so there is none — same treatment as mustDeployer's IsCurrent arm.\n\tif tcBase \u003e maxBase {\n\t\tpanic(\"kourtv2: this chain's own clock reads past 2100; check its genesis time\")\n\t}\n\ttcFloor = tcBase\n\t// The height freezes here too, for the same reason the clock does: a skew\n\t// added to a LIVE height cannot express a boundary, and its floor falls\n\t// behind by every block the chain produces between advances.\n\ttcHeightBase = runtime.ChainHeight()\n\ttcHeightFloor = tcHeightBase\n}\n\n// EnableTestClockAt arms the clock at a CHOSEN instant instead of the deploying\n// block's own time. A scenario file that declares its base produces the same\n// dates on every run — without this, tcBase is whatever the node happened to\n// think when the arming transaction landed, so absolute dates drift on every\n// reseed and no scenario can assert one.\nfunc EnableTestClockAt(cur realm, base int64) {\n\t// Deployer first, before the argument is even read. mustDeployer's own comment\n\t// gives the reason — a stranger must learn nothing here — and an\n\t// argument-validity oracle is that same shape even when the only secret is a\n\t// constant. EnableTestClock checks again below; a redundant gate is the safer\n\t// failure mode, and it keeps this entrypoint's first line the same as every\n\t// other crossing function's in the realm.\n\tmustDeployer(cur)\n\tif base \u003c= 0 {\n\t\tpanic(\"kourtv2: the test clock's base must be a positive instant\")\n\t}\n\tif base \u003e maxBase {\n\t\tpanic(\"kourtv2: the test clock's base must be an instant before 2100\")\n\t}\n\tEnableTestClock(cur)\n\ttcBase = base\n\t// The floor takes the HIGHER of the two. EnableTestClock has just captured\n\t// the chain's own instant; a declared base BELOW it used to overwrite the\n\t// floor downward. That is safe today only because arming demands a pristine\n\t// realm, so no stamp exists yet to be rewound below — a coincidence, not a\n\t// guard, and the moment the pristine test is loosened it becomes the same\n\t// class of rewind an audit proved for height (see tcHeightBase).\n\tif base \u003e tcFloor {\n\t\ttcFloor = base\n\t}\n}\n\n// AdvanceTestClock moves the clock forward by secs. Deployer only: arming must\n// not hand the steering wheel to the world, which the first draft did.\nfunc AdvanceTestClock(cur realm, secs int64) {\n\tmustDeployer(cur)\n\tif !tcArmed {\n\t\tpanic(\"kourtv2: the test clock is not armed\")\n\t}\n\tif secs \u003c= 0 || secs \u003e maxAdvanceStep {\n\t\tpanic(\"kourtv2: the test clock moves forward, by at most ten years a step\")\n\t}\n\tif tcSkew \u003e maxAdvanceTotal-secs {\n\t\tpanic(\"kourtv2: the test clock has advanced as far as it may\")\n\t}\n\ttcSkew += secs\n\tif tcSkew \u003e tcPeakSkew {\n\t\ttcPeakSkew = tcSkew\n\t}\n\tif n := tcBase + tcSkew; n \u003e tcFloor {\n\t\ttcFloor = n\n\t}\n}\n\n// AdvanceTestHeight moves the block height forward by n, for the accounting\n// clocks: conviction per block, emission periods, vote windows, twap maturity.\n// Deployer-only and forward-only, like its wall-clock twin.\nfunc AdvanceTestHeight(cur realm, n int64) {\n\tmustDeployer(cur)\n\tif !tcArmed {\n\t\tpanic(\"kourtv2: the test clock is not armed\")\n\t}\n\tif n \u003c= 0 || n \u003e maxHeightStep {\n\t\tpanic(\"kourtv2: the test height moves forward, by at most two emission periods a step\")\n\t}\n\tif tcHeightSkew \u003e maxHeightTotal-n {\n\t\tpanic(\"kourtv2: the test height has advanced as far as it may\")\n\t}\n\ttcHeightSkew += n\n\tif tcHeightSkew \u003e tcHeightPeak {\n\t\ttcHeightPeak = tcHeightSkew\n\t}\n\t// From the FROZEN base, not from a live read — the floor and heightNow()\n\t// must compute the identical expression or the floor falls behind by every\n\t// block the chain produces between advances.\n\tif h := tcHeightBase + tcHeightSkew; h \u003e tcHeightFloor {\n\t\ttcHeightFloor = h\n\t}\n}\n\n// TestHeightSkew is the blocks the height is currently advanced (0 once sealed).\nfunc TestHeightSkew() int64 { return tcHeightSkew }\n\n// TestHeightPeakSkew is the furthest it was ever advanced, kept past sealing.\nfunc TestHeightPeakSkew() int64 { return tcHeightPeak }\n\n// SealTestClock closes the window for good. Deployer only, and it does NOT\n// rewind: the floor keeps the highest instant this chain ever showed, so every\n// stamp already written stays in the past. Without that, sealing after a long\n// advance threw stamps into the future and REOPENED windows that had expired.\nfunc SealTestClock(cur realm) {\n\tmustDeployer(cur)\n\t// Refused on a realm that was never armed. Sealing is ONE-WAY, so a seal\n\t// typed at the wrong chain permanently forecloses ever arming that realm,\n\t// and there is nothing to seal — the clock is already the chain's own.\n\t// Deployer-only, so this is a footgun rather than a vulnerability; it is\n\t// also the only irreversible one with no upside.\n\tif !tcArmed {\n\t\tpanic(\"kourtv2: the test clock was never armed; there is nothing to seal\")\n\t}\n\tsealTestClock()\n}\n\n// sealTestClock is the internal one-way latch.\nfunc sealTestClock() {\n\ttcSealed = true\n\ttcArmed = false\n\ttcSkew = 0\n\ttcBase = 0\n\t// The height skew is dropped but its FLOOR is kept, so heights already\n\t// written into state stay in the past. Dropping the floor too would let a\n\t// sealed chain report a height below one it had already published, and\n\t// every window keyed on a stored height would silently reopen.\n\ttcHeightSkew = 0\n}\n\n// TestClockActive reports whether the clock can still be DRIVEN — armed and not\n// yet sealed. It is a capability question, not a disclosure one: a sealed test\n// chain answers false while still being full of invented dates. Ask\n// TestClockFabricated before believing a date on this chain.\nfunc TestClockActive() bool { return tcArmed }\n\n// TestClockFabricated reports whether this chain's dates were ever set by hand.\n// True from the moment the clock is armed and true forever after, including\n// past sealing, because sealing removes the steering wheel and not the history.\n// This is the accessor Render and any front end should key their warning on.\nfunc TestClockFabricated() bool { return tcEverArmed }\n\n// TestClockSkew is the seconds the clock is currently advanced (0 once sealed).\nfunc TestClockSkew() int64 { return tcSkew }\n\n// TestClockPeakSkew is the furthest the clock was ever advanced, kept past\n// sealing so a seeded chain can still say how far its dates were pushed.\nfunc TestClockPeakSkew() int64 { return tcPeakSkew }\n\n// tcNow is the clock every published deadline reads. Never rewinds below the\n// floor, and never returns the zero sentinel that clock.gno reads as \"this\n// claim predates the stamps\".\nfunc tcNow() int64 {\n\tvar n int64\n\tif tcArmed {\n\t\tn = tcBase + tcSkew // frozen: only AdvanceTestClock moves it\n\t} else {\n\t\tn = time.Now().Unix()\n\t\tif n \u003c tcFloor {\n\t\t\tn = tcFloor // a sealed test chain never falls back below what it showed\n\t\t}\n\t}\n\t// An assertion of last resort, and deliberately kept after maxBase made the\n\t// wrap that reached it unreachable through the entrypoints. Zero is\n\t// clock.gno's \"this claim predates the stamps\" sentinel, so a clock quietly\n\t// handing it out does not fail — it silently reclassifies every live claim as\n\t// pre-stamp. Refusing to answer is the correct behaviour for a clock that\n\t// cannot say what time it is.\n\tif n \u003c= 0 {\n\t\tpanic(\"kourtv2: the clock read a non-positive instant\")\n\t}\n\treturn n\n}\n"},{"name":"tier.gno","body":"// The claim's draw multiplier, derived from its size against the court's own\n// typical claim.\n//\n// WHAT THIS REPLACES. A sealed three-bucket vote — low, mid, high — riding a\n// dispute or a flag, with a bond, a bounty, a slash and a counter-flag window\n// behind it. It answered \"was this claim worth answering?\" and multiplied the\n// whole claim draw by 0, 1 or 2. It cost a second ballot on every disputed\n// claim, which is the thing this removes.\n//\n// THE SUBSTITUTE IS THE MONEY ITSELF. A claim capital showed up for is worth\n// more than one nobody staked, and the court already measures that: xBarFrozen\n// is the claim's time-averaged TOTAL stake, yes and no together, snapshotted at\n// the answer and already the base every bond and slash is priced off. So the\n// multiplier reads it instead of asking.\n//\n// WHY IT REACHES A STAKER AT ALL, which is the point of the change. A winner's\n// slice is\n//\n//\tgross = drawWinners × myConv / winPoolConv\n//\t      = (tier × midGross × 80/93) × myConv / midGross\n//\t      = tier × myConv × 0.86\n//\n// because midGross IS winPoolConv — the pool cancels. Without a multiplier the\n// same 10 CC earns the same on a 100 CC claim and a 1000 CC claim. `tier` is the\n// only free term in that expression, so it is the only place a claim's size can\n// reach a staker's rate.\n//\n// CONTINUOUS, NOT BANDED. A step function has a bar, and crossing a bar\n// re-prices the crosser's WHOLE pre-existing position in one jump — the last\n// unit of stake before the line buys a windfall on every unit already there. A\n// continuous multiplier has no such jump.\n//\n// WHAT IS ACCEPTED, said once and plainly. This is buyable at the margin: a\n// holder who stakes their own claim raises its multiplier, including on their\n// own position, and PostAnswer has no no-position gate so they may answer it\n// themselves. The bound is the token's distribution — the multiplier cannot be\n// lifted without locking a real fraction of supply on the claim, on a side, for\n// its life. The owner has ruled that bound sufficient. It is the same bound the\n// vote had, where a holder could carry a tally with weight they merely HELD.\npackage kourtv2\n\n// tierBpsFor is the claim's draw multiplier in bps: tierParBps is 1×.\n//\n// Reads only frozen state, so the answer is fixed the moment the claim is\n// answered and no later caller can move it. That is not a nicety: OpenRewards\n// is permissionless once the participants' week has passed, so a multiplier\n// derived from anything live would let whoever calls it choose the moment, and\n// therefore the number.\n//\n// The clamps are not decoration:\n//\n//   - the FLOOR exists because a claim that reaches this function already\n//     cleared effMinAnswerX, which is the court certifying it was worth\n//     resolving. Paying zero on a claim the same measure just admitted would be\n//     two thresholds on one quantity contradicting each other — and it would\n//     leave the answerer, who posted a bond and carries the risk, drawing\n//     nothing on a claim they were allowed to answer.\n//\n//   - the CEILING is the old high band exactly. tierHighX was 2, so a 2×\n//     ceiling keeps the maximum draw per claim, and with it the emission\n//     ceiling every deploy invariant is fitted to, where it already is. This\n//     re-keys which claims reach the top; it does not raise the top.\nfunc tierBpsFor(cs *claimState) int64 {\n\tif cs.tierRef \u003c= 0 {\n\t\treturn tierParBps // pre-answer, or a claim frozen before this shipped\n\t}\n\tv := mulDiv128(cs.xBarFrozen, tierParBps, cs.tierRef)\n\tif v \u003c tierFloorBps {\n\t\treturn tierFloorBps\n\t}\n\tif v \u003e tierCeilBps {\n\t\treturn tierCeilBps\n\t}\n\treturn v\n}\n\n// tierRefAt is the claim size that earns exactly 1×, resolved at the answer and\n// then frozen onto the claim.\n//\n// THE COURT'S OWN TYPICAL CLAIM, so \"big\" means big HERE. A fixed supply\n// fraction cannot do that: it would call every claim in a small-claims court\n// low and every claim in a large one high, and the number itself would be a\n// guess. Against the court's own average, a claim of average size earns par by\n// construction, in every court, with nothing to pick by hand.\n//\n// FLOORED ON A SUPPLY FRACTION, and the floor does two jobs at once:\n//\n//  1. IT IS THE BOOTSTRAP. A new court's accumulator is empty, so the floor is\n//     the reference until real claims have accumulated. There is no \"wait for N\n//     claims\" special case and no divide-by-zero — the court simply behaves like\n//     a fixed-reference one at first and re-centres as it fills.\n//\n//  2. IT IS THE ANTI-GAMING BOUND, and this is the load-bearing half. A LOW\n//     reference raises everybody's multiplier, so the cheap attack is to drag\n//     the average DOWN with a run of dust claims rather than to grow one claim.\n//     Answering costs a bond each, but the floor removes the question: the\n//     reference cannot be pushed below it, so the cheapest route to the ceiling\n//     is always real capital on the claim itself.\nfunc tierRefAt(c *Court) int64 {\n\tref := c.xBarAcc / tierEMAN\n\tif floor := supplyFloor(c, tierRefFloorBps, tierRefDustCC); ref \u003c floor {\n\t\tref = floor\n\t}\n\treturn ref\n}\n\n// observeClaimSize folds one answered claim's size into the court's running\n// average. Called once, at the freeze, AFTER the answering claim has taken its\n// own reference — a claim does not move the average it is measured against.\n//\n// AN ACCUMULATOR, NOT A MEAN, because the obvious integer EMA stalls. Written\n// `ema += (x - ema) / N`, every update where |x - ema| \u003c N truncates to zero and\n// the average sticks forever a few units from the truth — on a court whose\n// claims are all within N base units of each other, that is every update.\n// Holding N×ema and dividing only on read keeps the residue.\n//\n// O(1), which is the other requirement: the alternative is walking the docket at\n// every answer, and that grows without bound.\nfunc observeClaimSize(c *Court, xbar int64) {\n\tc.xBarAcc = mustAdd(c.xBarAcc, xbar-c.xBarAcc/tierEMAN)\n}\n\n// ClaimTierBps quotes the multiplier a claim's draw is scaled by, in bps of 1×.\n//\n// Quoted because it is the one term that decides what a staker's conviction is\n// worth on this claim rather than another, and it is not derivable from\n// anything else a page can read: it needs the court's average at the moment\n// this claim was answered, which is frozen on the claim and nowhere else.\nfunc ClaimTierBps(courtSlug string, claimID uint64) int64 {\n\treturn tierBpsFor(mustClaim(mustCourt(courtSlug), claimID))\n}\n\n// ClaimTierRef quotes the claim size that would have earned 1× on this claim —\n// the court's typical claim as of this claim's answer.\n//\n// The companion to ClaimTierBps: a multiplier without its reference cannot be\n// checked against anything, and the reference moves per claim.\nfunc ClaimTierRef(courtSlug string, claimID uint64) int64 {\n\treturn mustClaim(mustCourt(courtSlug), claimID).tierRef\n}\n\n// spamNetBps is the spam discount, in bps of 1x — the SECOND multiplier on a\n// claim's draw, and the two compose rather than compete.\n//\n//\ttierBpsFor   what the claim's SIZE says it is worth      (the money proxy)\n//\tspamNetBps   what the voters said about it, if any voted (the human read)\n//\n// A DISCOUNT, NEVER A REPLACEMENT, and that is the whole reason it is written\n// this way. Size reaches 2x and this caps at 1x, so if spam replaced size a\n// small self-staked claim (0.25x) that got disputed and drew no spam would jump\n// to 1.00x — a 4x promotion for being disputed, bought for the price of a\n// dispute bond by a second address, since OpenDispute bars only the answerer.\n// Composed, a dispute can lower a claim and can never promote one.\n//\n// The shape is 1 - 2s where s is the spam share, so it reaches zero exactly at\n// the half the discard threshold uses (dispute.gno). The two constants are one\n// constant seen twice; the deploy invariant in court.gno says so.\n//\n//\t 0% spam -\u003e 1.00x      33% -\u003e 0.34x\n//\t10%      -\u003e 0.80x      40% -\u003e 0.20x\n//\t25%      -\u003e 0.50x      50% -\u003e 0     (the claim stands and draws nothing)\nfunc spamNetBps(cs *claimState) int64 {\n\t// NO DECIDED ROUND, NO DISCOUNT. Zero here means \"nobody voted on this\",\n\t// which is every undisputed claim and every claim decided before this\n\t// shipped — not \"the voters said nothing, so pay nothing\". Reading it the\n\t// other way would zero the draw of every claim that never went to a vote,\n\t// which is most of them.\n\tif cs.spamTotalW \u003c= 0 {\n\t\treturn tierParBps\n\t}\n\tnet := cs.spamTotalW - mustMul(2, cs.spamW)\n\tif net \u003c= 0 {\n\t\treturn 0\n\t}\n\treturn mulDiv128(net, tierParBps, cs.spamTotalW)\n}\n"},{"name":"votelock.gno","body":"package kourtv2\n\nimport \"gno.land/p/nt/bptree/v0\"\n\n// THE VOTE LOCK — coin you voted with cannot leave your balance until the question\n// it decided has resolved.\n//\n// WHY, AND THIS WAS MEASURED ON THE SHIPPED FLOOR RATHER THAN ARGUED. The floor\n// (w = min(snapshot, BalanceOf)) requires holding at the MOMENT of voting and says\n// nothing about the moment after. A cast ballot is never undone, so the rental\n// survived as a reorder: borrow, wait one epoch to seal, open the dispute\n// yourself, VOTE WHILE STILL HOLDING, and repay in the very next call. Probed on\n// the committed floor at 599x the quorum floor with a final balance of zero —\n// against the 600x the original characterization measured. The floor moved the\n// attack one step to the right; it did not price it.\n//\n// This is what prices it. A renter must now carry the position across the whole\n// voting-and-resolution window rather than one epoch — roughly a week instead of\n// an hour, and the price of the attack is the risk of holding that long.\n//\n// THREE CORRECTIONS from the reverted first attempt, each from a panel finding:\n//\n//  1. UNTIL RESOLUTION, not the ballot close. The reverted version ended at\n//     p.closes, so a voter could sell before ResolveDispute applied the verdict\n//     they had caused. The predicate below reads the claim's own disputeOpen.\n//  2. THE AMOUNT VOTED, not the address. The reverted version was a bare height\n//     comparison on the address, so one dust abstain froze a voter's entire future\n//     inventory for a round.\n//  3. NO BOND CREDIT. The reverted version credited a nominator's own ballot-line\n//     bond as weight, which at votable \u003c= 20 CC made bond == floor and handed out\n//     a zero-cost moderator coup. Escrowed coin does not vote — which is what\n//     votableAt already says.\n//\n// IT BLOCKS DISPOSAL, NOT STAKING, and that distinction is deliberate. All SEVEN\n// mustSpendable call sites move coin OUT of the holder's balance — the claim\n// deposit, the answer bond, the dispute bond, the flag bond, the election\n// nomination bond, and the two transfer paths in lock.gno — so all seven respect\n// both locks. Stake is the single exemption and does not go through mustSpendable\n// at all: it calls mustStakable, because staking is a pure lock that leaves the\n// coin where it is and keeps it voting, and a pending vote is no reason to refuse\n// it.\n//\n// mustStakable therefore has EXACTLY ONE caller, and that is a safety invariant\n// rather than a fact about today's code: a second site reaching for it would be a\n// second path escaping the vote lock. check-epoch-coherence arm 6 pins the count.\n// The default direction is the safe one — a site nobody remembers to move stays\n// over-restrictive, never under.\n//\n// WHAT IT COSTS ON THE HOT PATH, and what I could not measure. voteLockedOf runs\n// inside disposable(), so every one of the seven mustSpendable call sites now walks\n// one address's lock rows. The cost is O(rows for that address), and the row count is\n// bounded twice: at most one row per question that address has voted in and which was\n// still open at its last vote, and lockVote prunes every resolved row on the next\n// vote. PruneVoteLocks exists for the address that votes and then stops. Measured\n// directly rather than argued: six open disputes voted and all six resolved leaves\n// six rows, and voting once more collapses it to one.\n//\n// THE GAS, MEASURED. A per-call figure is not readable directly: the harness's\n// `--- GAS:` lines cannot be attributed to a crossing call, because one value per test\n// is the test TOTAL and its POSITION in the stream varies between runs (two readings of\n// \"the last line is the transfer\" gave 489,076 and 7,163,199 for the same transfer; a\n// first pass compared two totals and suggested a 477x regression that does not exist).\n// What IS reliable is the total — it is the MAX of the lines, one per test — so the\n// cost of a single call is a DIFFERENCE of two totals over fixtures that differ by\n// exactly that call. Stable to the byte across runs:\n//\n//\tTransferCC with  0 lock rows        812,842 gas\n//\tTransferCC with 20 dead rows      3,017,747        110,245 per row\n//\tTransferCC with 40 dead rows      5,653,751        121,022 per row\n//\tTransferCC with 20 OPEN rows      1,194,543         19,085 per row\n//\tPruneVoteLocks over 20 dead rows  3,711,090        185,554 per row\n//\n// Linear in rows with a small tree-depth factor, not quadratic. Dead rows cost 5.8x\n// what open ones do, and that is the short-circuit in voteLockedOf: `w \u003e most` skips\n// voteLockOpen once some row has set a high-water mark, which open rows of equal weight\n// do on the first row and dead rows never do. So the pessimistic figure is the\n// dead-row one, and the backlog it prices is exactly what the prune clears.\n//\n// WHY ACCUMULATION CANNOT BRICK AN ADDRESS, also measured rather than argued. Rows are\n// self-inflicted: all three lockVote sites pass cur.Previous().Address(), so no third\n// party can grow another address's row count and tax its transfers. And lockVote walks\n// the rows before adding one, so the vote that creates row n+1 costs MORE than a\n// transfer at n rows — 2,952,537 against 1,194,543 at twenty rows, a factor of 2.47.\n// An address therefore reaches \"cannot vote\" about 2.5x sooner in row count than\n// \"cannot transfer\", and the transfer path keeps that much headroom at every row count\n// actually reachable. For scale, the 3B block ceiling\n// (tm2 MaxBlockMaxGas) is ~24,800 dead rows away, each of which costs a vote in a\n// separate concurrently-open question.\n//\n// RELEASE IS LAZY. voteLockedOf reads only the rows whose question is still open,\n// so nothing iterates a whole electorate at resolution — that would put an\n// O(voters) cost on a permissionless call, which is a griefing surface of its own.\n// Resolved rows are pruned when the same address next votes, bounding the rows per\n// address by the number of questions it has open at once.\n//\n// THE QUALITY LANE IS LOCKED TOO, and the shape of its lock is the answer to what\n// looked like an unanswerable question.\n//\n// The problem, as it appeared: the quality tally ACCUMULATES. reaskQualityTally\n// bumps the seq on every round while nothing has been adjudicated, then re-asks\n// EXACTLY ONCE after the first adjudication and never again — so from that point\n// the seq is frozen, every later round votes the same running total, and each\n// address is pinned to the one vote it already cast. There is no single \"round\n// resolved\" instant to release at, which is why this was written off as an owner\n// decision.\n//\n// The answer is to stop looking for an instant and state the actual principle: THE\n// COMMITMENT LASTS EXACTLY AS LONG AS THE INFLUENCE. A quality vote stops\n// influencing anything the moment its tally is superseded — voted keys on\n// qVoteSeq|addr, so once openQualityTally bumps the seq, the old vote counts toward\n// nothing. And if the seq never moves again, the vote keeps deciding every\n// remaining round, so the commitment should last until the claim itself is over.\n//\n// THE FIRST VERSION OF THIS GOT THE PRINCIPLE RIGHT AND THE PREDICATE WRONG. It read\n// `not terminal AND cs.qVoteSeq == the seq voted in`, and claimed that in the ordinary\n// pre-adjudication case \"each round re-asks, so the seq moves and the lock releases\n// almost immediately — no 12-week freeze for voting on an ordinary flag.\" The seq\n// moves when a round OPENS, and nothing makes a round open. A failed-quorum dispute\n// parks the claim: verdictAt == 0, closed == false, disputeOpen == false, round == 1,\n// failedRounds 1 of the 3 that would provisionally close it — and that third only\n// arrives if somebody volunteers two more dispute bonds.\n//\n// The first pass at this concluded the state was a DEAD END and that the freeze was\n// unbounded. That was wrong, and it was wrong the way an incomplete enumeration is\n// always wrong: SettleUndisputed refuses (round \u003e 0), CloseDeadClaim refuses (the\n// claim has an answer), ResolveDispute refuses (nothing open) — three routes checked,\n// and Finalize never tried. Finalize is exactly the route, and the design knew it; the\n// comment on its tier default says \"a failed dispute round keeps\n// verdictAt=0/decidedRounds=0 so Finalize is reachable in that window\".\n//\n// So the honest measurement, which is smaller than the story and still worth fixing:\n// the freeze is bounded by the escrow window plus, for anyone who is not a participant\n// in the claim, finalizeGraceBlocks. Probed at 155,521 blocks left on the escrow\n// window at the moment the round failed, plus the 120,960-block grace — about sixteen\n// days in which an ordinary quality voter's ENTIRE balance was committed, measured at\n// 20,000,000,000 of 20,000,000,000 with disposable 0, for a vote that could not decide\n// anything ever again. They cannot shorten it either: Finalize is participant-only for\n// that first week and they are not a participant.\n//\n// So the predicate has to ask whether the vote can still DECIDE anything, which is a\n// different question from whether its tally is still the current one:\n//\n//\topen  iff  the claim is not terminal\n//\t           AND cs.qVoteSeq == the seq voted in\n//\t           AND (some question is open now  OR  these weights carry forward)\n//\n// Both new arms are needed. Without the first, the vote about to be counted would\n// release while it is being counted. Without the second, the frozen-accumulating\n// case — precisely where a divested address was measured deciding rounds it never\n// held through — would release while the same running total keeps being voted.\n//\n// It is also the hardest lane to rent into regardless, because the anchor is the\n// ANSWERER's: using it means answering the claim yourself and voting from a second\n// address.\n\nconst (\n\tvoteLockDispute  = \"d\"\n\tvoteLockElection = \"e\"\n)\n\n// voteLockKey is addr|kind|id|sub, with both ids fixed-width big-endian so that\n// one address's rows are a contiguous range and can be scanned without touching\n// anyone else's.\nfunc voteLockKey(who address, kind string, id, sub uint64) string {\n\treturn string(who) + kind + beClaimKey(id) + beClaimKey(sub)\n}\n\n// voteLockOpen decodes one row's key and asks whether the question it names has\n// resolved. A row whose question cannot be found at all is treated as RESOLVED:\n// the alternative is coin locked forever by a claim that no longer exists.\n// voteLockParts unpacks a row key. Factored out when CommitmentsOf started needing\n// the same three fields the predicate needs: two decoders of one key format is the\n// shape that drifts, and the shape this design has spent a dozen commits pinning\n// against.\nfunc voteLockParts(who address, key string) (kind string, id, sub uint64, ok bool) {\n\ttail := key[len(string(who)):]\n\tif len(tail) != 1+8+8 {\n\t\treturn \"\", 0, 0, false // not a shape this code wrote\n\t}\n\treturn tail[0:1], beClaimID(tail[1:9]), beClaimID(tail[9:17]), true\n}\n\nfunc voteLockOpen(c *Court, who address, key string) bool {\n\tkind, id, sub, ok := voteLockParts(who, key)\n\tif !ok {\n\t\treturn false // do not hold coin on a key this code did not write\n\t}\n\tswitch kind {\n\tcase voteLockDispute:\n\t\tv := c.claims.Get(beClaimKey(id))\n\t\tif v == nil {\n\t\t\treturn false\n\t\t}\n\t\tcs := v.(*claimState)\n\t\t// The SAME round, not merely any open dispute on the claim. A later round\n\t\t// is a different question, and the ballot this row is holding coin for was\n\t\t// already applied when its own round resolved.\n\t\treturn cs.disputeOpen \u0026\u0026 cs.proposalID == int64(sub)\n\tcase voteLockElection:\n\t\tif c.mod == nil || c.mod.election == nil {\n\t\t\treturn false\n\t\t}\n\t\te := c.mod.election\n\t\treturn e.seq == id \u0026\u0026 !e.resolved\n\t// THE QUALITY LANE STOOD HERE. It held a voter's coin while a quality tally\n\t// could still count it, keyed on qVoteSeq, and released once the tally could no\n\t// longer decide anything. Nothing writes qVoteSeq now, so the case was\n\t// unreachable — and an unreachable lock arm is worse than none: it reads as a\n\t// live hold and would be the first thing a future reader copied.\n\t}\n\treturn false\n}\n\n// voteLockedOf is the CC this address must keep in its balance because it voted\n// with it in a question that has not resolved.\n//\n// THE MAX ACROSS OPEN QUESTIONS, NOT THE SUM, and this was a shipped bug found by\n// probing rather than reading. Each row is a floor — \"at least this much of your\n// balance must stay put\" — and one pile of size max(rows) satisfies every row at\n// once, because the same coins can honour more than one commitment. Summing\n// double-counts them.\n//\n// It was not a corner case. P11 makes the quality ride its own transaction, so\n// voting a verdict and then its quality question is the ORDINARY thing an engaged\n// holder does — and measured, that holder ended up with 40 B committed against a\n// 20 B balance and could move nothing at all, having committed one pile once. This\n// is the same sum-vs-max error as between the stake lock and the vote lock in\n// lock.gno's disposable(), one level down, and the lesson is that a \"total locked\"\n// quantity built by addition is worth distrusting on sight.\nfunc voteLockedOf(c *Court, who address) int64 {\n\tif c.voteLocks == nil {\n\t\treturn 0\n\t}\n\tvar most int64\n\tpre := string(who)\n\tc.voteLocks.Iterate(pre, pre+\"\\xff\", func(k string, v any) bool {\n\t\tif w := v.(int64); w \u003e most \u0026\u0026 voteLockOpen(c, who, k) {\n\t\t\tmost = w\n\t\t}\n\t\treturn false\n\t})\n\treturn most\n}\n\n// lockVote records that `amount` of who's balance is committed to a question, and\n// prunes any of that address's rows whose questions have since resolved.\n//\n// Idempotent per (address, question): a second call for the same key OVERWRITES\n// rather than adding, because a voter votes a question once and a re-entry would\n// otherwise double-lock them. The three call sites each refuse a second vote\n// before reaching here, so this is a belt on top of a brace.\nfunc lockVote(c *Court, who address, kind string, id, sub, amount int64) {\n\tif amount \u003c= 0 {\n\t\treturn\n\t}\n\tif c.voteLocks == nil {\n\t\tc.voteLocks = bptree.NewBPTree32()\n\t}\n\tpruneVoteLocks(c, who)\n\tc.voteLocks.Set(voteLockKey(who, kind, uint64(id), uint64(sub)), amount)\n}\n\n// pruneVoteLocks drops the rows whose questions have resolved, and returns how\n// many it dropped.\n//\n// IT CANNOT CHANGE ANY BALANCE OR ANY COMMITMENT, which is what makes the exported\n// wrapper safe to leave permissionless. It removes exactly the rows voteLockedOf\n// already skips — the predicate is the same voteLockOpen in both — so the committed\n// figure before a prune and after it are equal by construction, and\n// TestPruningDropsDeadRowsWithoutMovingTheCommitment asserts that rather than\n// trusting it.\n//\n// WHY IT NEEDS AN ENTRYPOINT AT ALL. lockVote prunes, so the common case cleans\n// itself: an address that keeps voting never carries dead rows. An address that\n// votes in several simultaneously-open questions and then STOPS carries one row per\n// question, forever. Measured: six open disputes voted, all six resolved, six rows\n// still there with ZERO coin committed, and every mustSpendable on that address\n// walking all six. Bounded only by how many questions were open at once, which a\n// court with many live claims does not bound usefully — and each row is persistent\n// state paying a storage deposit for nothing.\n//\n// Collect before removing: the tree must not be modified during iteration.\nfunc pruneVoteLocks(c *Court, who address) int {\n\tif c.voteLocks == nil {\n\t\treturn 0\n\t}\n\tvar stale []string\n\tpre := string(who)\n\tc.voteLocks.Iterate(pre, pre+\"\\xff\", func(k string, _ any) bool {\n\t\t// A KEY THIS CODE CANNOT PARSE IS NOT THIS HOLDER'S ROW, AND NOT OURS TO\n\t\t// DELETE. Treating an unparseable key as stale is what turned a short\n\t\t// `who` into a permissionless wipe: `address` is a bare string type with\n\t\t// no length invariant, so a caller passing \"g1\" prefix-matched every\n\t\t// holder's rows, voteLockParts rejected each on length, voteLockOpen\n\t\t// returned false, and every live commitment in the court was removed as\n\t\t// stale. Probed on the shipped code: a stranger's one call dropped a\n\t\t// voter's 10,000,000,000 commitment and the voter then moved all of it\n\t\t// after voting with it, which is the rental this lock exists to price.\n\t\t// The entry points validate the address now too; this is the half that\n\t\t// holds even if one of them ever forgets.\n\t\tif _, _, _, ok := voteLockParts(who, k); !ok {\n\t\t\treturn false\n\t\t}\n\t\tif !voteLockOpen(c, who, k) {\n\t\t\tstale = append(stale, k)\n\t\t}\n\t\treturn false\n\t})\n\tfor _, k := range stale {\n\t\tc.voteLocks.Remove(k)\n\t}\n\treturn len(stale)\n}\n\n// beClaimID is beClaimKey's inverse, for reading an id back out of a composite\n// key. Kept next to nothing else: the only reason it exists is that voteLockOpen\n// has to know which question a row belongs to.\nfunc beClaimID(s string) uint64 {\n\tvar v uint64\n\tfor i := 0; i \u003c 8; i++ {\n\t\tv = v\u003c\u003c8 | uint64(s[i])\n\t}\n\treturn v\n}\n\n// PruneVoteLocks drops `who`'s resolved lock rows and returns the count, so an\n// address that voted in several questions and then stopped is not left walking dead\n// rows on every transfer.\n//\n// PERMISSIONLESS, and deliberately so: it removes only rows that voteLockedOf\n// already ignores, so it cannot free committed coin or change what anyone may\n// spend. A third party paying gas to shrink someone else's state is doing them and\n// the chain a favour, and there is nothing for a griefer to gain — the worst\n// available move is cleaning up.\n//\n// Takes `who` rather than acting on the caller for the same reason: it is\n// maintenance, not a privilege.\nfunc PruneVoteLocks(cur realm, courtSlug string, who address) int {\n\tif !cur.IsCurrent() {\n\t\tpanic(errStaleRealm)\n\t}\n\t// AFTER the frame check, which check-stale-guards requires to come first and was\n\t// right to: a stale-realm caller is refused before anything else is considered.\n\t// `address` is a bare string type with no length invariant, so an unvalidated\n\t// `who` here prefix-matched every holder's rows and the prune removed them all.\n\tif !who.IsValid() {\n\t\tpanic(\"kourtv2: that is not a valid address\")\n\t}\n\treturn pruneVoteLocks(mustCourt(courtSlug), who)\n}\n\n// CommitmentsOf is one read for everything `who` is committed to in this court.\n//\n//\t\"stake:\u003cn\u003e;vote:\u003cn\u003e;free:\u003cn\u003e\" plus one \";q:\u003ckind\u003e:\u003cid\u003e:\u003csub\u003e:\u003camount\u003e\" per\n//\topen vote commitment, in the house format ClaimTimeline established.\n//\n// stake is the stake lock, vote is the vote lock, free is what may actually\n// leave the balance — the same figure DisposableOf returns and the one every\n// transfer, bond and deposit is checked against.\n//\n// THE q ROWS MAY SUM TO MORE THAN `vote`, AND THAT IS NOT A BUG. `vote` is the\n// MAX across open rows, not the sum: the locks overlap, because one pile of coin\n// honours many commitments at once. Adding the q rows up produces a number the\n// court does not enforce. `free` is the enforced figure.\n//\n// Only OPEN commitments are listed; a question that has resolved leaves no row.\n// Stake positions are not listed here — StakedPage answers where a holder's\n// money is, this answers what constrains their next move.\n//\n// Refused only on a court that does not exist. Costs about 19k gas per open row.\nfunc CommitmentsOf(courtSlug string, who address) string {\n\tc := mustCourt(courtSlug)\n\tout := \"stake:\" + itoa(lockedOf(c, who)) +\n\t\t\";vote:\" + itoa(voteLockedOf(c, who)) +\n\t\t\";free:\" + itoa(disposable(c, who))\n\tif c.voteLocks == nil {\n\t\treturn out\n\t}\n\tpre := string(who)\n\tc.voteLocks.Iterate(pre, pre+\"\\xff\", func(k string, v any) bool {\n\t\t// Closed rows are not commitments. They are also the expensive ones to\n\t\t// walk, so skipping them is both correct and the cheap direction.\n\t\tif !voteLockOpen(c, who, k) {\n\t\t\treturn false\n\t\t}\n\t\tkind, id, sub, ok := voteLockParts(who, k)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\tout += \";q:\" + kind + \":\" + itoa(int64(id)) + \":\" + itoa(int64(sub)) +\n\t\t\t\":\" + itoa(v.(int64))\n\t\treturn false\n\t})\n\treturn out\n}\n\n// VoteLockedOf is the CC this address cannot dispose of because it voted with it\n// in a question that has not resolved. Quoted because a holder who cannot transfer\n// deserves to know why, and how much.\nfunc VoteLockedOf(courtSlug string, who address) int64 {\n\treturn voteLockedOf(mustCourt(courtSlug), who)\n}\n"},{"name":"voteweight.gno","body":"package kourtv2\n\n// THE ONE VOTE-WEIGHT EXPRESSION IN THIS REALM.\n//\n//\tw = min( PastVotes(who, the question's own frozen epoch), BalanceOf(who) )\n//\n// The snapshot is the CEILING: you cannot buy in after the question was asked. The\n// live balance is the FLOOR: you cannot vote weight you have handed back. Neither\n// half works alone — two earlier designs each kept one and failed, one to a rental\n// and one to a live numerator against a frozen bar. VOTEFLOOR.md has the history.\n//\n// WHY IT IS A FUNCTION AND NOT THREE INLINE COPIES. Three lanes charge this weight\n// and a reader QUOTES it to the elector, and a quote that can drift from the charge\n// is worse than no quote at all — the holder plans against a number the vote will\n// not honour. One expression, six call sites — the quality lane, the election lane,\n// both readers, the explainer, and the verdict lane through voteCap — and\n// check-epoch-coherence arm 4 pins that there is exactly one definition of it.\n//\n// BalanceOf, never spendable(). Staked coin is still in the holder's balance and\n// still votes; spendable() nets the stake out, so using it here would disenfranchise\n// every staker in the court — the court-wide disenfranchisement v0.34 deleted, and\n// the single most-repeated mistake in this design's history.\n//\n// COHERENT BY CONSTRUCTION, because a floor can only LOWER a figure that was\n// already coherent:\n//\n//\tΣ min(PastVotes(·,E), BalanceOf(·))  ≤  Σ PastVotes(·,E)  =  PastTotal(E)\n//\n// so every tally stays under the bar it is measured against, provided the bar is\n// drawn at the same E. That last identity is the ledger's own and is asserted\n// directly by TestPastVotesSumToPastTotal.\n//\n// Returns BOTH figures because the two zero cases mean different things to a\n// holder — \"you had nothing when this was asked\" and \"you no longer hold what you\n// had\" are different problems with different remedies — and a single return value\n// would have collapsed them into one message.\nfunc votingWeight(c *Court, who address, at uint32) (w, snapshot int64) {\n\tsnapshot = c.coin.PastVotes(who, at)\n\tw = snapshot\n\tif held := c.coin.BalanceOf(who); held \u003c w {\n\t\tw = held\n\t}\n\tif w \u003c 0 {\n\t\tw = 0\n\t}\n\treturn w, snapshot\n}\n\n// voteCap is the ceiling the DISPUTE lane hands the governor, and it lives here so\n// that every live balance read in this realm is in one file.\n//\n// That lane cannot use votingWeight: the governor owns the verdict tally and\n// derives the snapshot itself, so what it must be given is a CEILING it can lower\n// to — never a finished weight. An earlier attempt handed it a weight, which is not\n// drawn from p.total, so `cast` could exceed it, `rest := p.total - cast` went\n// negative, and `no` dropped out of the early-decide test entirely. The two are the\n// same number by construction (the governor computes min(PastVotes(who, p.epoch),\n// cap) and votingWeight computes min(PastVotes(who, at), BalanceOf(who)) for the\n// same epoch), which is what lets ClaimVoteWeightOf quote the verdict lane honestly\n// without duplicating the expression — and TestTheQuoteMatchesTheCharge pins it.\nfunc voteCap(c *Court, who address) int64 { return c.coin.BalanceOf(who) }\n\n// ClaimVoteWeightOf quotes what `who` can actually cast on a claim's two open\n// questions, right now: the verdict (weighed at the dispute round's own epoch) and\n// the quality tally (weighed at the answer's). Zero means either no snapshot or\n// nothing still held — VoteWeightWhy says which.\n//\n// Quoted because a holder refused at the door deserves the number, and because\n// \"how much am I worth here\" is not derivable from a balance: the answer depends on\n// an epoch the holder cannot see and on which questions happen to be open.\nfunc ClaimVoteWeightOf(courtSlug string, who address, claimID uint64) (verdict, quality int64) {\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tif cs.disputeOpen {\n\t\tverdict, _ = votingWeight(c, who, c.gov.EpochOf(cs.proposalID))\n\t}\n\treturn verdict, quality\n}\n\n// ElectionVoteWeightOf quotes what `who` can cast in the open election, and it is\n// the frozen figure once they have approved anything: the first approval fixes a\n// voter's weight for the whole ballot, so a later quote must report what was\n// frozen and not what a fresh derivation would give.\nfunc ElectionVoteWeightOf(courtSlug string, who address) int64 {\n\tc := mustCourt(courtSlug)\n\tif c.mod == nil || c.mod.election == nil || c.mod.election.resolved {\n\t\treturn 0\n\t}\n\te := c.mod.election\n\tif prior := e.voters.Get(who.String()); prior != nil {\n\t\treturn prior.(int64)\n\t}\n\tw, _ := votingWeight(c, who, e.epoch)\n\treturn w\n}\n\n// VoteWeightWhy explains a zero, which is the only part of this a holder cannot work\n// out for themselves — ONE ANSWER PER LANE, mirroring ClaimVoteWeightOf.\n//\n// The first version returned a single string and picked the dispute's epoch whenever\n// a dispute was open. That was wrong in a reachable state, and measured: with a\n// dispute open BOTH questions are live (VoteQuality accepts votes while\n// cs.disputeOpen) at DIFFERENT epochs — the verdict at the proposal's, the quality\n// ride at the answer's. A holder who bought after the answer but before the dispute\n// opened has a 0 quality snapshot and a positive verdict one, and the single-string\n// version told them \"weighed at the full snapshot\" while VoteQuality was about to\n// refuse them outright. Measured at quality 0 against verdict 10 B.\n//\n// Two lanes, two epochs, two answers. Returning a pair makes that structural instead\n// of something a reader has to remember, and it matches the shape of the figures in\n// ClaimVoteWeightOf so the two readers cannot drift apart in what they cover.\nfunc VoteWeightWhy(courtSlug string, who address, claimID uint64) (verdict, quality string) {\n\tc := mustCourt(courtSlug)\n\tcs := mustClaim(c, claimID)\n\tverdict = \"no verdict vote is open on this claim\"\n\tif cs.disputeOpen {\n\t\tverdict = whyWeighed(c, who, c.gov.EpochOf(cs.proposalID))\n\t}\n\tquality = \"the quality vote no longer exists\"\n\treturn verdict, quality\n}\n\n// whyWeighed is the explanation for ONE epoch. Split out so the two lanes cannot\n// share an answer by accident, which is exactly how the first version went wrong.\n//\n// WRITTEN FOR SOMEBODY WHO HAS NEVER USED THIS. These four sentences are the\n// first thing a newcomer reads when the court turns them away, and they used to\n// be written in the vocabulary of the thing that refused them — \"at the epoch\n// this question was frozen at\", \"there is nothing to weigh\", \"weighed at the\n// full snapshot\". Epoch, snapshot and weigh are all internal words. What a\n// reader needs is when the vote started, what they held then, and whether\n// buying now would help.\n//\n// The first arm's opening — \"you had none when this vote started\" — is matched\n// by the overlay to tell the fixable case from the hopeless one, so it is\n// pinned by check-web-constants. Reword it and the build says so.\nfunc whyWeighed(c *Court, who address, at uint32) string {\n\tw, snapshot := votingWeight(c, who, at)\n\tswitch {\n\tcase snapshot \u003c= 0:\n\t\treturn \"you had none when this vote started; coin bought since counts in later votes\"\n\tcase w \u003c= 0:\n\t\treturn \"you had some when this vote started but have none now, and your say is the smaller of the two\"\n\tcase w \u003c snapshot:\n\t\treturn \"limited to what you still hold, which is less than when this vote started\"\n\t}\n\treturn \"counted in full\"\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserAll"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":"ifN+PskVfRGKrncnDf79PJ3DOzGSLM3C4dMiejKhsBdTs0IQWhwUs+YasGBhIR4tsn78xOAhDh4TpzxwGs4oKQ=="}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"EnableTestClockAt","args":["1579046400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SetSiteDomain","args":["kourt.xyz"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"StartCourt","args":["covid","COVID-19 Origins \u0026 Response Court"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1c88ydxxf56gpvns0pemggj536yct7vpc9556ac","send":"554400000ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3n1D5jeha5vV0JCizx9sUYg4Dy3HaeQKMPTVzvFyPHI"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"655200000ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1yxfv5xkdlht9jc9kaeqku29jfgwxsj2knmv6ym","send":"655200000ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A9qenm5JgE7Ztqv3EJC73fmVuKQTGwvELmzWJmsSyv/E"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"705600000ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"705600000ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"756000000ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ygrl7wtdvqk9jap5k42vhg0c06vun48qpdg7wa","send":"756000000ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Ak3bszZQxcX6SKN0DUqw1Um7yArUG6TkFtGYEDlYcL18"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"756000000ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"806400000ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"856800000ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ap7p83he8hxzysla0z3gcpvvy72jgkvqrh6wkl","send":"856800000ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AvK1IUjaAF7Gr7mPxY1pWLt09XMEq/g9tyXAtVzNAUN/"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"907200000ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"957599999ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"1007999999ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"1058399999ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1nrhrxfcz89l9psh44cmc30mjfds8ukn023r2ts","send":"1058399999ugnot","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Buy","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyfAin8AWROgeLZ+1/8OT+/kvAF06FOsy95YKA6yRTiy"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","𓁼 Origins","Where the virus came from, and who built what. Each hypothesis is its own claim, so none is settled by another one losing."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","1","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","𓁼 Fauci","What NIAID funded under his direction, what his office wrote, and what he said in private and in public. Sorted below by the kind of record each claim settles on."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","2","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","𓁼 Vaccine safety claims","What the shots did in pregnancy. Two of these are false and are here to be answered — the only way a docket disposes of a number."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","3","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","𓁼 Gain-of-function funding","The grant record, and the definitional fight over what it was."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","4","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","𓁼 Proximal Origin","The 2020 paper: who prompted it, what its authors thought while writing it, and who else was organising at the same time."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","5","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","𓁼 The iPhone texts","34,000 messages off a government device, released August 2026 — and the claims made about them, which are not all the same claim."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","6","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","1","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","2","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","3","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","4","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","5","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","6","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","1","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","2","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","3","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","4","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","5","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","6","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","1","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","2","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","3","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","4","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","5","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","6","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259260"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["51840"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","1"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"New","args":["covid","1"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","2"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"New","args":["covid","2"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","3"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"New","args":["covid","3"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","4"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"New","args":["covid","4"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","5"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"New","args":["covid","5"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","6"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"New","args":["covid","6"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"MoveFolder","args":["covid","4","2"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"MoveFolder","args":["covid","5","2"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"MoveFolder","args":["covid","6","2"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SetFolderImage","args":["covid","2","img|756a9d89538b94c4368a33c1bf77d554114867d9135da9742df7b6e17c162ddc|image/jpeg|960|1344|184593|Anthony S. Fauci, NIAID director (NIAID, CC BY 2.0)|https://raw.githubusercontent.com/jaekwon/cryptocourt/main/scenarios/assets/fauci.jpg"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["12700800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","The furin cleavage site in SARS-CoV-2 has no close analogue in the sampled sarbecovirus record.","Settles on an identified natural progenitor carrying the motif, or on documentary evidence of insertion. Neither exists, which is why this one is expected to close unanswered."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","1","7"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","7","1","5000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","7","0","12000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","7","0","6000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","7","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["432000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","7","0","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["432000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","7","1","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Unstake","args":["covid","7","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["432000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","7","1","7000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["432000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","7","1","6000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["432000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Unstake","args":["covid","7","0","6000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["518400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","7","1","5000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["691200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Unstake","args":["covid","7","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["777600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","7","1","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["777600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","7","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["777600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","7","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["1468800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"CloseDeadClaim","args":["covid","7"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["518400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","SARS-CoV-2 was released deliberately as a Chinese state bioweapon.","Settles on evidence of deliberate release as a weapon. The published case rests on the Yan Li-Meng reports; no review has sustained them.\n\nAsks ONLY about deliberate release. Whether the virus was engineered is a different claim and is docketed separately in this folder, some of it running the other way — a NO here rejects the weapon story, not the laboratory one.\n\nFiled because it is one of the most circulated claims about this pandemic, and a docket that will not hear the popular claim is not a court."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","1","8"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","8","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","8","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["120960"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","8","0","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","8","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1nrhrxfcz89l9psh44cmc30mjfds8ukn023r2ts","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","8","0","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyfAin8AWROgeLZ+1/8OT+/kvAF06FOsy95YKA6yRTiy"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","8","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","8","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","8","1","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1nrhrxfcz89l9psh44cmc30mjfds8ukn023r2ts","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","8","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyfAin8AWROgeLZ+1/8OT+/kvAF06FOsy95YKA6yRTiy"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","8","1","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","Every published version of the bioweapon claim traces to the Yan Li-Meng preprints, which no peer review has sustained.","Settles on the citation record: every published version of the bioweapon claim traces to the two Zenodo preprints, and on whether any peer-reviewed venue has sustained them.\n\nFiled against the claim above rather than in place of it."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","1","9"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","9","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","9","1","9000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","8","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","8","1"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","8"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","9","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["777600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","9","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","9","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","9"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["16588800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","US federal grants funded coronavirus research at the Wuhan Institute of Virology.","Settles on the grant record: the subawards from EcoHealth Alliance to the Wuhan Institute of Virology, and the NIH acknowledgements of them."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","4","10"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","10","0","11000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","10","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["777600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","10","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["777600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","10","1","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","Vaccination in the first or second trimester was followed by miscarriage in 82% of completed pregnancies.","Settles on the arithmetic in the source table: the 82% comes from dividing the losses by only those pregnancies that had already ended, when most women vaccinated early were still pregnant at the cutoff.\n\nOne of the most-repeated numbers of the pandemic, and one of the easiest to check. That is why it is here: a court that cannot settle a false number this plain cannot settle anything."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","3","11"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","11","0","9000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","11","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","10","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","11","0","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","10"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","11","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","11","0","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","11","1","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","11","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ap7p83he8hxzysla0z3gcpvvy72jgkvqrh6wkl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","The 82% figure divides completed-pregnancy losses by a denominator that excludes pregnancies still ongoing.","Settles on the published table: 104 losses against a denominator of 127 counts only those vaccinated in the first or second trimester, most of whom had not finished their pregnancies."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AvK1IUjaAF7Gr7mPxY1pWLt09XMEq/g9tyXAtVzNAUN/"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","3","12"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ap7p83he8hxzysla0z3gcpvvy72jgkvqrh6wkl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","12","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AvK1IUjaAF7Gr7mPxY1pWLt09XMEq/g9tyXAtVzNAUN/"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","12","1","10000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ap7p83he8hxzysla0z3gcpvvy72jgkvqrh6wkl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","11","1","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AvK1IUjaAF7Gr7mPxY1pWLt09XMEq/g9tyXAtVzNAUN/"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","11","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","11","1","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","The organiser of the February 2020 Lancet statement concealed his institute's funding relationship with the Wuhan Institute of Virology.","Settles on the statement's own published correction and the signatory record."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","5","13"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","13","0","9000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","13","1","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","11","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","11","1"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["120960"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ap7p83he8hxzysla0z3gcpvvy72jgkvqrh6wkl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","12","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AvK1IUjaAF7Gr7mPxY1pWLt09XMEq/g9tyXAtVzNAUN/"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","11"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","13","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","12","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","12","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","13","1","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","12"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","13","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","13"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["2073600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","mRNA COVID-19 vaccination before 20 weeks of pregnancy raises the risk of miscarriage above the 10-20% population baseline.","Settles on cohort data against a population baseline. The final CDC analysis reports 10.79% across 12,097 vaccinated pregnancies, with no elevated risk week by week to twenty weeks; baseline is roughly 10 to 20%."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","3","14"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","14","0","6000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","14","1","5000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","14","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","14","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ap7p83he8hxzysla0z3gcpvvy72jgkvqrh6wkl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","14","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AvK1IUjaAF7Gr7mPxY1pWLt09XMEq/g9tyXAtVzNAUN/"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","14","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","14","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ygrl7wtdvqk9jap5k42vhg0c06vun48qpdg7wa","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","14","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Ak3bszZQxcX6SKN0DUqw1Um7yArUG6TkFtGYEDlYcL18"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ap7p83he8hxzysla0z3gcpvvy72jgkvqrh6wkl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","14","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AvK1IUjaAF7Gr7mPxY1pWLt09XMEq/g9tyXAtVzNAUN/"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","14","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","14","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","14","1"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","14"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["1468800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","The 2018 DEFUSE proposal describes inserting a furin cleavage site into a SARS-related bat coronavirus.","Settles on the document. DRASTIC published the 2018 DARPA proposal in September 2021; the text either describes the insertion or it does not.\n\nAsks ONLY what the proposal says. Whether the work was carried out is a different claim, and DARPA did not fund this one."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","1","15"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","15","0","10000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","15","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["777600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","15","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["777600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","15","1","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","15","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","15"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["12528000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","Ralph Baric's laboratory constructed the virus that became SARS-CoV-2.","Settles on documentary or sequence evidence tying a construct made at that laboratory to SARS-CoV-2. Raised as a question by Sachs and others; nobody has evidenced it."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","1","16"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","16","0","7000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","16","1","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["120960"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","16","0","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","16","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","16","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","16","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","16","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","16","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","16","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","16","1","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","16","0","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","16","1"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","16"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["24451200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","The Proximal Origin authors privately assessed the genome as showing engineered features in the week they drafted the paper rejecting that.","Settles on the released correspondence, which is public and unredacted.\n\nThe best-evidenced claim in this folder, and the one the corpus most often garbles into something larger."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","5","17"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","17","0","7000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","17","1","5000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["777600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","17","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["777600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","17","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","17","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","17"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["5097600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","A laboratory-associated origin is the more likely explanation, on the evidence public in 2023.","Settles on a finding by a body with subpoena power, or a published determination the relevant experts do not contest.\n\nDeliberately asks about the balance of evidence PUBLIC IN 2023, not the eventual truth: a claim whose answer depends on documents nobody has is unanswerable, and this docket already carries two that died that way."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","1","18"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","18","1","6000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","18","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","18","1","7000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","18","0","6000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","18","1","9000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","18","0","5000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","18","1","6000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Unstake","args":["covid","18","0","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","18","1","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["432000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","18","0","9000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["432000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Unstake","args":["covid","18","1","5000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["518400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","18","0","5000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["518400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","18","1","7000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["604800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Unstake","args":["covid","18","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["604800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","18","1","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["691200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","18","0","6000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["691200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Unstake","args":["covid","18","1","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["1296000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"CloseDeadClaim","args":["covid","18"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["27216000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","Ralph Baric was excluded from the Proximal Origin author list on the ground that he was too close to the Wuhan Institute of Virology.","Settles on the authors' own testimony to congressional investigators, which is on the record.\n\nFiled because it cuts against the claim above from the same body of evidence that is usually cited FOR it: the party closest to the construction work was kept off the paper, deliberately, and said so."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","1","19"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","19","0","6000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","19","1","5000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["777600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","19","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["777600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","19","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","19","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","19"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["66009600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","In January 2021 the director of NIAID privately raised first-trimester miscarriage as a theoretical risk of the second vaccine dose.","Settles on the message itself, released 10 August 2026 from a government device produced to a Senate subcommittee.\n\nAsks only whether the message says what it is quoted as saying. What follows from it is the two claims after this one."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","6","20"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","20","0","5000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","20","1","6000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","The January 2021 miscarriage concern was withheld from the public while the same officials recommended vaccination in pregnancy.","Settles on comparing the private chain with the public statements over the same period.\n\nThe same message chain, one day later, weighs risks against benefits and records ten thousand vaccinated pregnancies with no signal — so the private position and the public one have to be compared whole, not by their first line."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","6","21"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","21","0","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","21","1","7000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["345600"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","21","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","21","1","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["120960"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","20","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","21","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259200"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","21","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","21","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","21","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","20","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","21","0","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","21","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","The NIAID-funded work at the Wuhan Institute of Virology met the federal P3CO definition of gain-of-function research.","Settles on a determination by HHS or another authorised body that the funded work did or did not meet the P3CO definition.\n\nThe sharpest claim in this folder, because it is the one thing everybody is actually arguing about: nobody disputes that the money reached the work. NIH conceded in 2021 that a limited experiment met some criteria while rejecting the label."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","4","22"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","22","0","6000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","22","1","5000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","20","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","21","0","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","21","1"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","The Proximal Origin paper was drafted at the prompting of the director of NIAID.","Settles on the correspondence together with sworn testimony from the participants. The 1 February 2020 teleconference and the drafting timeline are documented; whether they amount to prompting is what is contested."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","5","23"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","23","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","23","1","7000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","22","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","Testimony given to Congress in 2024 denying participation in intelligence discussions about Wuhan research was false.","Settles on a perjury referral producing a finding, or an authoritative determination by a body with subpoena power. An accusation attached to a document release is not a finding."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","4","24"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","24","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","24","1","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","20"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","The same message chain records, one day later, more than ten thousand vaccinated pregnancies with no adverse signal.","Settles on the same released chain that carries the quoted line.\n\nFiled because a quotation with its reply removed is a different claim from the quotation, and the docket should hold both."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","6","25"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","25","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","25","1","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","21"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","22","1","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","23","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","23","1","5000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","24","0","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","22","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","24","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","23","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","22","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","24","0","4000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","22","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","23","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","25","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","22","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","23","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","24","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","22","0","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","23","1","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","24","0","3000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","22","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","23","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","24","1","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["172800"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","22","0","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","23","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","24","0","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1yxfv5xkdlht9jc9kaeqku29jfgwxsj2knmv6ym","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","22","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A9qenm5JgE7Ztqv3EJC73fmVuKQTGwvELmzWJmsSyv/E"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","25","1","2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenDispute","args":["covid","22"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","23","0","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","24","1","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1yxfv5xkdlht9jc9kaeqku29jfgwxsj2knmv6ym","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","23","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A9qenm5JgE7Ztqv3EJC73fmVuKQTGwvELmzWJmsSyv/E"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenDispute","args":["covid","23"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","24","0","1000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1yxfv5xkdlht9jc9kaeqku29jfgwxsj2knmv6ym","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","24","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A9qenm5JgE7Ztqv3EJC73fmVuKQTGwvELmzWJmsSyv/E"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","25","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenDispute","args":["covid","24"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["86400"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","9","8","contests"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","19","16","contests"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","25","21","contests"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ap7p83he8hxzysla0z3gcpvvy72jgkvqrh6wkl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","12","11","contests"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AvK1IUjaAF7Gr7mPxY1pWLt09XMEq/g9tyXAtVzNAUN/"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","15","7","supports"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","15","18","supports"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","10","22","supports"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","10","18","supports"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","17","23","supports"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","13","23","supports"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","20","21","supports"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","17","24","supports"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","19","23","supports"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","14","21","contests"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ap7p83he8hxzysla0z3gcpvvy72jgkvqrh6wkl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","12","14","contests"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AvK1IUjaAF7Gr7mPxY1pWLt09XMEq/g9tyXAtVzNAUN/"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","7","8","supports"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddAssociation","args":["covid","18","7","supports"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ApproveAssociation","args":["covid","15","18"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"DisapproveAssociation","args":["covid","7","8"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","1","10"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenRewards","args":["covid","8"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"WithdrawBonus","args":["covid","8","1"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"WithdrawBonus","args":["covid","8","1"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenRewards","args":["covid","21"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"WithdrawBonus","args":["covid","21","1"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenRewards","args":["covid","15"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"WithdrawBonus","args":["covid","15","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"BuyCommentPass","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"BuyCommentPass","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"BuyCommentPass","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"BuyCommentPass","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"BuyCommentPass","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ap7p83he8hxzysla0z3gcpvvy72jgkvqrh6wkl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"BuyCommentPass","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AvK1IUjaAF7Gr7mPxY1pWLt09XMEq/g9tyXAtVzNAUN/"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"BuyCommentPass","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"BuyCommentPass","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"BuyCommentPass","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1yxfv5xkdlht9jc9kaeqku29jfgwxsj2knmv6ym","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"BuyCommentPass","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A9qenm5JgE7Ztqv3EJC73fmVuKQTGwvELmzWJmsSyv/E"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"BuyCommentPass","args":["covid"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostComment","args":["covid","22","0","The P3CO framework is the standard the claim names, so the question is not whether the work was risky but whether it met that definition. The 2017 guidance is the text to read."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1yxfv5xkdlht9jc9kaeqku29jfgwxsj2knmv6ym","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostComment","args":["covid","22","0","I answered YES on the reading that the enhanced-transmissibility clause is met. The bond is on that reading, not on the wider question."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A9qenm5JgE7Ztqv3EJC73fmVuKQTGwvELmzWJmsSyv/E"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostComment","args":["covid","22","0","The grant text and the framework use different words for the same thing in two places. Which one governs is the whole dispute."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostComment","args":["covid","22","1","The 2017 guidance also carves out surveillance work, and that carve-out is what the department leaned on at the time."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostComment","args":["covid","22","3","Agreed that it is the dispute. The grant text is the later document, which is why I filed it that way."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"UpvoteComment","args":["covid","22","2"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"UpvoteComment","args":["covid","22","2"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"UpvoteComment","args":["covid","22","2"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"UpvoteComment","args":["covid","22","3"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostComment","args":["covid","22","0","Posting a working link to the grant PDF here; I will replace it if the host moves it."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"HideOwnComment","args":["covid","22","6","true"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ap7p83he8hxzysla0z3gcpvvy72jgkvqrh6wkl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostComment","args":["covid","22","0","This has nothing to do with the claim and everything to do with a different argument three claims over."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AvK1IUjaAF7Gr7mPxY1pWLt09XMEq/g9tyXAtVzNAUN/"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"HideBoardRow","args":["covid","22","7","off-topic"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1yxfv5xkdlht9jc9kaeqku29jfgwxsj2knmv6ym","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostComment","args":["covid","24","0","The answer rests on the transcript as published, not on the summary that was reported from it."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A9qenm5JgE7Ztqv3EJC73fmVuKQTGwvELmzWJmsSyv/E"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostComment","args":["covid","24","1","The published transcript and the released recording differ in one line, and it is the line the claim turns on."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"OpenClaimP","args":["covid","𓂀 Furin cleavage site","Whether the site has a natural analogue is the question three claims here already turn on; this heading gathers them."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AddToFolder","args":["covid","1","26"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","26","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","26","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["720"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"Stake","args":["covid","26","0","8000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"PostAnswer","args":["covid","26","0"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestClock","args":["259260"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"AdvanceTestHeight","args":["51840"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SettleUndisputed","args":["covid","26"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SetCourtCreationBurn","args":["2000000"]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1lmldyy0fn4vqdr6ccr5dejd6ux5nq2w3dngz3y","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Aj28xW+/jMBsDyOJe6rvcbSkwWINjTRpciIXbq02xEVU"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1gtdusn4d6k2633plcz6d968uf4xyjq5wcc52ms","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A5hSabt/YxTxO4zQyE+2RgWRdVCtQcmmJ0pB37vArdho"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g18l9f69nynlexm46sky50pshpcvycnrrcywxavr","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+NC2LWj/sFzi59wnrkgfbZF9MoBcRJyCluSOXJpRzac"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10h424d3cn7lgcekyg44k54w2ncmfnfcp7mx9zz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyoTYq5xKKtTBzLNN3YbA5y/YVLt/4lSzYVhdmRoqxy3"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g10gwcc9u3hluwxcu77m5pkekd8lyly3xjxqx8hy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3YZS6O5ZaXXJIzzXtdgyXYfE7J0AoSrZNFPpiDjQwPO"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1kwrjqhvatu85p404mcsd3vcm2xw82f99uqesnv","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhetDSsc1eaZUVRxx4rS9XoHYGS6MYWz2Qui3+KyvCiG"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g154t8adpjgv746dc6z43x3thg3dwd3jfua5mc7v","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AzTI3QzZZb95ELFk7lisD766DySTtlRmqkSCS0N1NL0k"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1pca27q6f9s0hhaf9rkr92dnr7e8v2p05ju7hvy","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ArP2gufcglP6wT4DoXZdgvYwZlJnDYMAq1KttE5fPIdN"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ygrl7wtdvqk9jap5k42vhg0c06vun48qpdg7wa","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Ak3bszZQxcX6SKN0DUqw1Um7yArUG6TkFtGYEDlYcL18"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1ap7p83he8hxzysla0z3gcpvvy72jgkvqrh6wkl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AvK1IUjaAF7Gr7mPxY1pWLt09XMEq/g9tyXAtVzNAUN/"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1c88ydxxf56gpvns0pemggj536yct7vpc9556ac","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A3n1D5jeha5vV0JCizx9sUYg4Dy3HaeQKMPTVzvFyPHI"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1me5nrjdkaqerrh5s47syz387jhqsxwqn4wv9wc","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AwNMvwDprxedVzSViiol23cnXFdf5yPPDsEiTwM5XjvA"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1cf62m6e9mp57qnw7wty9565hulcukm8azy5ryl","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"ApbU2p3DGKMW5AdbeKxahmM9/N6w0nBGEmZHws95w3pT"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1nrhrxfcz89l9psh44cmc30mjfds8ukn023r2ts","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AyfAin8AWROgeLZ+1/8OT+/kvAF06FOsy95YKA6yRTiy"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jazl3tfrqvnykasuf5c6tjaxtnh769l369gxf7","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AgUInGQ2vzP8rLr3w3nOo67bszanO5vN2MiHRB0sEyEW"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1yxfv5xkdlht9jc9kaeqku29jfgwxsj2knmv6ym","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"ClaimMetaFranchise"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A9qenm5JgE7Ztqv3EJC73fmVuKQTGwvELmzWJmsSyv/E"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SetCourtDesc","args":["meta","This court's coin is not received for GNOT — it is earned. Every burn for any other court's coin accrues here to whoever made it, one for one, and waits as an entitlement until they claim it."]}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g174hxvmvs7fsg50gy7xu8x9eyqa4chdv8tclngz","send":"","max_deposit":"","pkg_path":"gno.land/r/kourt/kourtv2","func":"SealTestClock"}],"fee":{"gas_wanted":"200000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6CtcB+u/MOE+nAfup7wHhRvDMmvDXDLvZAdID0FiT+t"},"signature":null}],"memo":""}}]}}}}