Search Apps Documentation Source Content File Folder Download Copy Actions Download

cache.gno

7.44 Kb · 200 lines
  1package validators
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"sort"
  7	"strconv"
  8
  9	"gno.land/p/nt/bptree/v0"
 10	"gno.land/p/sys/validators"
 11	sysparams "gno.land/r/sys/params"
 12)
 13
 14// valopersRealmPath is the only realm allowed to refresh valoperCache
 15// or invoke RotateValoperSigningKey. Both auth checks below depend on
 16// being called via `cross` from a crossing function in valopers.
 17const valopersRealmPath = "gno.land/r/gnops/valopers"
 18
 19// valoperCache mirrors the (operator -> current signing key) view from
 20// r/gnops/valopers. Written by valopers via NotifyValoperChanged. Read
 21// by future operator-keyed proposal flow (step 5).
 22//
 23// Pushing (valopers passes the values as args) rather than pulling
 24// (v3 imports valopers and reads them) — pulling would create an
 25// import cycle, since valopers already imports v3 for IsValidator.
 26var valoperCache = bptree.NewBPTree32()
 27
 28type cacheEntry struct {
 29	SigningPubKey  string
 30	SigningAddress address
 31	KeepRunning    bool
 32}
 33
 34// assertValopersCaller panics if the caller realm is not r/gnops/valopers.
 35// Per docs/resources/gno-interrealm.md, this check works only when (a)
 36// the host function is a crossing function (`cur realm`) and (b) it's
 37// invoked via `cross` from a crossing function in valopers. Then
 38// PreviousRealm() shifts exactly one frame to valopers. A user MsgCall
 39// would see PreviousRealm() == UserRealm (pkgpath ""); a third realm
 40// cross-call would see its own pkgpath. Either fails this check.
 41func assertValopersCaller(_ int, rlm realm) {
 42	// Defense-in-depth IsCurrent gate. Current call sites pass live
 43	// cur (cross(cur) from valopers), so this never fires today — but
 44	// the helper trusts its rlm input, and any future call that threads
 45	// a stashed/sibling-frame realm value with .Previous().PkgPath() ==
 46	// valopersRealmPath would silently bypass the gate (Class-2
 47	// designation forgery; see docs/resources/gno-security.md). Gating
 48	// here makes the precondition enforceable rather than convention.
 49	if !rlm.IsCurrent() {
 50		panic("unauthorized: rlm is not the caller's live cur")
 51	}
 52	caller := rlm.Previous().PkgPath()
 53	if caller != valopersRealmPath {
 54		panic("caller realm must be " + valopersRealmPath + ", got " + caller)
 55	}
 56}
 57
 58// NotifyValoperChanged refreshes the cached entry for op. Auth: caller
 59// realm must be r/gnops/valopers.
 60//
 61// READ-ONLY against valopers: by design this function does not call
 62// back into valopers (no pull). Valopers pushes the current values in
 63// as args. This eliminates the confused-deputy class where v3 → valopers
 64// callbacks would make valopers see v3 as PreviousRealm.
 65func NotifyValoperChanged(cur realm, op address, signingPubKey string, signingAddress address, keepRunning bool) {
 66	assertValopersCaller(0, cur)
 67	valoperCache.Set(op.String(), cacheEntry{
 68		SigningPubKey:  signingPubKey,
 69		SigningAddress: signingAddress,
 70		KeepRunning:    keepRunning,
 71	})
 72}
 73
 74// RotateValoperSigningKey applies a signing-key rotation to the
 75// effective valset and publishes the new full set via sysparams.
 76// Auth: caller realm must be r/gnops/valopers.
 77//
 78// Body is read-modify-write against sysparams.GetValsetEffective so
 79// concurrent same-block writers (other rotations or GovDAO executors)
 80// accumulate instead of clobbering. Mirrors the executor pattern in
 81// validators.gno.
 82//
 83// Idempotent: if the rotating operator's old signing address is not
 84// currently in the effective valset (e.g., they were removed), the
 85// rotation is a no-op at the sysparams level — valopers' profile and
 86// signingRegistry already record the new key. Either replays cleanly.
 87//
 88// Emits ValoperRotated event with op + old/new addresses + height.
 89func RotateValoperSigningKey(cur realm, op address, oldPubKey, newPubKey string) {
 90	assertValopersCaller(0, cur)
 91
 92	oldAddr, err := chain.PubKeyAddress(oldPubKey)
 93	if err != nil {
 94		panic("invalid oldPubKey: " + err.Error())
 95	}
 96	newAddr, err := chain.PubKeyAddress(newPubKey)
 97	if err != nil {
 98		panic("invalid newPubKey: " + err.Error())
 99	}
100
101	baseline := sysparams.GetValsetEffective()
102	set := make(map[address]validators.Validator, len(baseline))
103	for _, v := range baseline {
104		set[v.Address] = v
105	}
106
107	// The rotating operator must currently be in the active set; if
108	// not (operator removed before rotating), nothing to publish.
109	// Valopers-side state has already been updated regardless.
110	prev, ok := set[oldAddr]
111	if !ok {
112		chain.Emit(
113			"ValoperRotated",
114			"op", op.String(),
115			"oldAddr", oldAddr.String(),
116			"newAddr", newAddr.String(),
117			"height", strconv.FormatInt(runtime.ChainHeight(), 10),
118			"applied", "false",
119		)
120		return
121	}
122
123	delete(set, oldAddr)
124	set[newAddr] = validators.Validator{
125		Address:     newAddr,
126		PubKey:      newPubKey,
127		VotingPower: prev.VotingPower,
128	}
129
130	// Defense-in-depth: a delete+insert in this branch always leaves
131	// at least one entry (the freshly inserted newAddr), so an empty
132	// set is unreachable today. Panic explicitly anyway: a future
133	// refactor of this body that ends up publishing an empty set
134	// would otherwise be silently swallowed by the EndBlocker
135	// (which logs and clears dirty for empty publishes), masking
136	// the regression.
137	if len(set) == 0 {
138		panic("rotation would empty the validator set; refused to keep consensus liveness")
139	}
140
141	entries := make([]string, 0, len(set))
142	for _, v := range set {
143		entries = append(entries, v.PubKey+":"+strconv.FormatUint(v.VotingPower, 10))
144	}
145	sort.Strings(entries)
146	sysparams.SetValsetProposal(cross(cur), entries)
147
148	chain.Emit(
149		"ValoperRotated",
150		"op", op.String(),
151		"oldAddr", oldAddr.String(),
152		"newAddr", newAddr.String(),
153		"height", strconv.FormatInt(runtime.ChainHeight(), 10),
154		"applied", "true",
155	)
156}
157
158// AssertGenesisValopersConsistent panics if any entry in valset:current
159// (the seeded genesis valset) lacks a corresponding valoperCache profile
160// whose SigningAddress matches.
161//
162// **Genesis-mode only.** The function refuses to run unless
163// runtime.ChainHeight() == 0. This is the documented intended usage
164// (last migration .jsonl tx, before any block has been produced) and
165// also closes a post-genesis MsgCall DoS surface — without the guard,
166// an attacker could pay gas to repeatedly invoke an O(N) iteration
167// over valoperCache + valset:current after the chain is live.
168//
169// gnoland's InitChainer auto-runs this assertion at end of
170// genesis-mode replay when GnoGenesisState.PastChainIDs is non-empty;
171// failure aborts the boot unconditionally. valoper-seed and
172// hand-crafted migration .jsonls do NOT need to emit the call
173// themselves.
174//
175// Crossing function: callable via MsgCall (only at genesis-mode).
176// Doesn't mutate state — pure invariant check. Inverse direction
177// (every valoperCache entry must have a corresponding valset:current
178// entry) is intentionally NOT checked: extra valoper profiles
179// registered without immediate valset inclusion are a normal
180// post-genesis state.
181func AssertGenesisValopersConsistent(cur realm) {
182	if runtime.ChainHeight() != 0 {
183		panic("AssertGenesisValopersConsistent is only callable during genesis-mode replay (ChainHeight()==0)")
184	}
185
186	// Collect the signing addresses present in valoperCache.
187	seen := map[string]bool{}
188	valoperCache.Iterate("", "", func(_ string, raw any) bool {
189		entry := raw.(cacheEntry)
190		seen[entry.SigningAddress.String()] = true
191		return false
192	})
193
194	// Every entry in valset:current must appear in seen.
195	for _, v := range sysparams.GetValsetEntries() {
196		if !seen[v.Address.String()] {
197			panic("genesis-validator " + v.Address.String() + " has no corresponding valoper profile (signing address not in valoperCache)")
198		}
199	}
200}