Files
consensus/validator
zeekayandHanzo Dev c9b9b94e99 validator: close the node axis, make the canonical order build-invariant
Red review of the oracle found the bytes unbreakable and the set-plane rule
around them incomplete. This closes it.

HIGH — one node, one key. Register enforced one-key-one-node but not the
converse: a slice could name one node twice under two keys, each with a genuine
proof, so possession did not catch it and one operator held several signer
indices and several shares of the weight under one identity. FlattenValidatorSet's
map input made that unrepresentable; a slice does not. A byNode set beside byKey
refuses it (ErrDuplicateNode), in the same pass, so the set is a set on both axes
— the rule weighted_merkle already states and registration.go had half of.

HIGH — one canonical order, on every build. PublicKeyBytes, the sort key, was the
UNCOMPRESSED encoding, which the crypto library returns as 96 bytes under blst
and 48 under CIRCL — so the same set sorted two ways, and two nodes on different
builds indexed the signer bits differently. It is the COMPRESSED encoding now, in
both Register and the local FlattenValidatorSet, so the order is the same on every
build. Under the canonical (purego) image this still matches upstream; under cgo
it is now deterministic where upstream is not. The crypto root cause is upstream
and live on the warp path — escalated separately, not fixed here.

Also: key uniqueness now keys on the compressed bytes decoded from the point, not
the caller's input buffer read three times (a caller could mutate it between the
verify and the map insert); a zero-weight validator is refused (a signer that
raises the count but not the weight, the phantom BuildWeightedValidatorSet already
refuses); cert-store eviction drops the file the evicted slot names, not whatever
the map points at, and unmaps a decision only if the map still names the removed
file; and a canonicalCommitter test block exercises the cert-store re-key in the
regime where the inner decision differs from the outer envelope — the case every
prior test degraded past.

Node width is 20 bytes, frozen in vectors/pop.json. Nothing here runs on mainnet:
node pins a consensus version without these files, and Register has no non-test
callers. A validator set that shares a key across node ids is now refused — the
intended behaviour, and the seam with the P-chain step still to come.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 08:10:35 -07:00
..