Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

cache.gno

7.45 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/v0"
 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// (v0 imports valopers and reads them) — pulling would create an
 25// import cycle, since valopers already imports v0 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 v0 → valopers
 64// callbacks would make valopers see v0 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}