zap.ListBuilder.AddBytes counts BYTES not elements; fixed-stride lists were
storing byte-inflated counts -> stride clamp rejected them. Fixed every write
helper to store the real element count (len(entries)). Round-trip test now
GREEN across the hardest cases: multisig+stakeable outputs/inputs, the nested
ConvertNetworkToL1Validator (NodeID+BLS PoP+2 owners), SovereignL1Chain
manifest, and signed unsigned‖creds. go build ./vms/platformvm/txs = exit 0.
The P-chain tx wire is now struct-IS-wire, codec-free, and verified correct.
All 21 real P-chain tx types are now the zap buffer — no codec, no marshal/
unmarshal, no Manager, no version, decompounded, on luxfi/zap. ConvertNetworkToL1Tx
+ CreateSovereignL1Tx hand-written with a shared ConvertNetworkToL1Validator
nested encoder (fixed-stride records + shared NodeID/addr pools) and a
SovereignL1Chain encoder. go build ./vms/platformvm/txs/ = exit 0.
Next: round-trip correctness test, then the external consumer flip.
- SlashValidatorTx ripped (unwired stub; Avalanche has no slashing; consensus
only detects, never enforces). Type+executor+tests+Visitor surface removed.
- CreateAssetTx + OperationTx ripped from P-CHAIN (X-chain/AVM types; LP-0130:
asset creation + fx ops are X-chain money-rail, not P staking-rail; no P
producer). xvm/avm untouched.
- Staker interfaces (StakerTx/ValidatorTx/DelegatorTx/ScheduledStaker) restored
on the 5 validator types via pure accessors + FxID-on-stake preserved.
- Remaining real complex types: ConvertNetworkToL1Tx + CreateSovereignL1Tx.
tx.go: Tx holds Unsigned (zap-backed) + Creds; Parse wraps signed bytes and
splits unsigned/creds at the self-delimiting boundary; Sign builds unsigned‖creds;
no codec.Manager param anywhere. Deleted codec.go (V0/V1/V2 reflection Manager)
and codec_activation.go. WIP: consumer sites that passed txs.Codec / called the
old Parse(codec, bytes) flip next.
Parse wraps the buffer zero-copy and dispatches on the 1-byte kind. Signed =
unsigned ‖ creds (both self-delimiting), so unsigned is a byte-prefix of
signed. writeCredsBuf/parseCredsBuf encode credentials natively (shared 65B
sig-blob array). No Marshal/Unmarshal/Manager. WIP: references the 24 pure
type constructors (5 hand-written + 18 in parallel conversion + 1 template).
Shared delta-field encoders every non-proposal tx composes on the spending
envelope: owner (fx.Owner), auth (*secp256k1fx.Input), inline Validator (44B)
+ Signer (145B: kind+BLS pubkey+PoP), id lists, extra spending lists. Built on
luxfi/zap. With spending.go, the full reusable foundation — type files are now
pure compositions.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The envelope (NetworkID/BlockchainID/Outs/Ins/Memo) + multisig/stakeable
Output/Input entries + shared owner-address/sig-index arrays, built on
luxfi/zap generic primitives — no codec, no zap_native package. writeSpending/
setEnvelope build inside New*Tx; readEnvelope reads lazily. Polymorphism
(TransferOutput/LockOut, TransferInput/LockIn) handled in explode/assemble.
Foundation every non-proposal tx composes.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
kind.go: 1-byte discriminator @ object offset 0 = the whole dispatch. No
codec, no version, no slot map. AdvanceTimeTx converted to the pure model:
holds *zap.Message, Time() is an offset read, NewAdvanceTimeTx builds once,
Bytes() returns the buffer. No marshal/unmarshal anywhere.
Template for the remaining 21 types. Package migrates atomically (codec.go +
all types + Parse/Sign together), so it compiles green again only when the
whole set + concentrated consumers (executor/builder/fee/api, ~63 New sites)
are converted. WIP.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
ZAP has no serialization step. The bridge copied fields between plain txs.*
structs and the buffer, which is exactly the codec ZAP deletes. Removed.
The right way: tx type IS the zap buffer (accessors, Parse=wrap, Bytes=buffer),
no codec / manager / compat / version. Wire primitives stay in luxfi/zap.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Shared spending envelope (NetworkID/BlockchainID/Outs/Ins/Memo) + polymorphic
converters bridging the txs struct graph to proto/zap_native primitives with
zero reflection: TransferOutput/stakeable.LockOut, TransferInput/stakeable.LockIn,
secp256k1fx.Credential. BaseTx (TxKindBaseFull) bridged; creds travel in the
separate creds buffer of the unsigned‖creds envelope.
Round-trip GREEN: 2-of-3 multisig + owner-locktime output, stakeable.LockOut,
stakeable.LockIn input, memo, AND signed secp256k1 credentials all survive
Marshal->Unmarshal with exact field equality; unsigned stays a byte-prefix of
signed. Every embedding tx type now composes this envelope + its delta fields.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Adds nativeManager implementing pcodecs.Manager by bridging txs structs to
proto/zap_native buffers — no reflection, no serialize-tag walk, no version
dispatch. Signed wire = unsigned_zap_buffer ‖ creds_zap_buffer (ZAP buffers
are self-delimiting), preserving tx.go's unsigned-is-prefix-of-signed
invariant with no tx.go change. AdvanceTimeTx + RewardValidatorTx bridged
and round-trip green (build->Marshal->Unmarshal field equality, byte-stable,
prefix invariant, non-ZAP reject). Added alongside the reflection Codec;
flip + reflection deletion lands once all registered types are bridged.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Third-layer mainnet finality fix. v1.34.27 (consensus v1.35.36) proved the
dynamic committee (alpha=4 n=5) and topology.go cert-receive canonical resolve
work live — consensus climbed past 1085755 to round-height 1085761 — but hit the
next layer: a stale-alias prevote-lock. viewForLocked seeds lockBlock from a
pre-canonical-fix durable committedSlot (an OUTER proposervm-wrapper id); a
round-0 lock on an outer alias makes prevoteTarget compare the stale outer id
against the inner-canonical winner by raw id, mismatch forever, no POL, freeze.
v1.35.37 (742696baf): stepViewChange rebases a stale-alias lock onto the inner
canonical winner IFF it resolves (via the same vmCanonicalResolver the cert
rebase uses) to exactly winnerCanon — a genuinely different inner (real fork) or
unresolvable lock is left fail-closed frozen, never merged. lockRound preserved,
2a-n>f untouched, alpha inner-precommits were always inner (CanonicalVoteMessage
binds inner, excludes outer). Full engine/chain green (356s, 0 fail); -race
clean; RED safety gate proves divergent inners still refused.
Roll note: luxd-4 first releases its lock (log 'vc stale-alias lock REBASED');
finality past 1085755 needs >=4 nodes on this build, so height climbs as 3->2->
1->0 come up. EVM stays v1.104.7 (head-pin).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Unsticks mainnet C-Chain finality. Root cause (consensus-layer, proven): under
pChainHeight=0 anyone-can-propose, each validator wraps the same inner block in
its own outer proposervm envelope; votes are canonical-keyed so an α-of-K cert
forms network-wide, but HandleIncomingCert did a strict envelope-id lookup — a
node holding a different alias of the same inner block missed it and requested
catchup instead of finalizing the local wrapper it held, so VM.Accept was never
called and the EVM head froze at 1085755 while consensus logged 'finalized
voters=4'. v1.35.34 (66438a23b) resolves the local wrapper by canonical id and
rebases the verified cert (outer ids unsigned; α votes verify unchanged), no
fork (per-height gate keys on canonical id, idempotent).
Also: dynamic committee 1→N proven (v1.35.35, effectiveCommittee sizes cert +
view-change from live validator count; n=1→1/1,2→2/2,3→3/3,4→3/4,5→4/5 — the
BFT-safe ⌊2n/3⌋+1); view-change safety gate realigned to the effective
committee (RED#2); snowman comment-purge (Quasar/Nova); dynamic committee logs.
Node commits: 4e71814e58 (proposervm→EVM Accept-cascade test, RED#1),
c13681108f (purge), a02611413e (logs). EVM stays v1.104.7 (head-pin). Full
engine/chain suite green under -race (369s, 0 fail/0 race); RED adversarial
suite proves scale-invariance 1→1M + RLP seam byte-exact on real mainnet state.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The mainnet 1085755 freeze had consensus finalizing an outer proposervm block while the
inner EVM lastAccepted stayed frozen. The consensus engine's ledger advancing on VM.Accept
is proven in engine/chain; this proves the OTHER half at the NODE layer — that VM.Accept on
a proposervm wrapper cascades to the inner block's Accept:
postForkBlock.Accept → acceptOuterBlk → acceptInnerBlk → Tree.Accept(innerBlk) →
innerBlk.Accept ⇒ inner VM lastAccepted advances
- TestAcceptCascade_InnerHeadAdvances_AtScale: 1000 heights through the REAL Accept path;
the inner EVM head + proposervm head advance in lock-step at every height.
- TestAcceptCascade_SiblingStorm_WinnerAdvancesInnerOnce: five distinct outer envelopes wrap
ONE inner block (the anyone-can-propose alias set); accepting the finalized wrapper advances
the shared inner exactly once.
Confirms the propagation was sound, not a node-layer bug — the freeze was the consensus-layer
storm-alias resolution gap (fixed in consensus v1.35.34). The real luxfi/evm RLP-import→produce
→tip byte-exactness is proven separately in evm/core rlp_seam_red_test.go; composed, they cover
the full engine→proposervm→EVM Accept path.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Removes the forbidden Avalanche term from code comments in chains/manager.go,
chains/quorum.go, and vms/proposervm/proposer/windower_determinism_test.go. Reworded to
preserve exact meaning; comments only, no identifier/behavior change.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The "round-scoped view-change ENABLED for chain K=21 alpha=15" line read as if K=21/α=15
were the finality committee — misleading. It is the Snowman SAMPLE preset. The α-of-K
cert and the view-change POL/precommit are sized to the LIVE validator set at runtime
(effectiveCommittee/bftCommittee; 5 validators → K=5/α=4), and the engine already logs
the effective (K,α) on each committee re-clamp. Relabel the fields presetK/presetAlpha
and add a note pointing at the runtime committee-clamp log. Log-only; no behavior change.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Restores mainnet C-Chain finality. The prior stall: with 5 live validators but
MainnetParams K=21/alpha=15, BOTH finality gates (assembleCertLocked cert-alpha
AND view-change POL) required 15 distinct votes — impossible from 5 validators,
so the chain froze (safe, no fork). consensus v1.35.33 sizes both gates from the
live validator count via effectiveCommittee (alpha=4 for n=5), inheriting the
minBFTCommittee K=4/alpha=3 floor (1085013 self-finality guard preserved);
Snowman K=21 sample untouched. node chains/quorum.go adds
validatorStakeSource.ValidatorCount (height-indexed, deterministic). EVM stays
v1.104.7 (head-state pin). Test: 5-validator MainnetParams control freezes,
fix converges in 0.35s.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Closes postmortem residual #1 (proposer-preference restart loop / mute-voter
wedge). Build-side companion to the defect #1 SetPreference validate-before-
assign hardening.
vm.preferred is only adopted after a successful getBlock, but a block fetchable
at preference time can later become unfetchable: an unaccepted sibling consensus
dropped, or a never-persisted outer block referenced after heavy sibling churn.
The old BuildBlock returned after the single getBlock(preferred) miss with
"failed to fetch preferred block", so every attempt failed in a tight loop and
the node's voter went mute (~170 err/s). Quasar cert-finality has no re-converge
poll, so the fleet never recovered on its own — the liveness wedge behind the
mainnet 1082879->1085755 window (heartbeat-pause + full-fleet-restart churn).
Fix: on an unfetchable preferred, build the child on last-accepted (always held:
committed state). The node keeps producing on a valid tip while the catch-up
path pulls the gap; a later SetPreference(held tip) re-advances the preference.
Only surface the original error when last-accepted is itself the unfetchable id.
Tests (hermetic, mirror vm_rejoin_wedge_test.go): spy inner VM asserts the fetch
sequence — BuildBlock consults last-accepted on an unheld preferred (old code
never did), and does not spuriously fetch when no distinct fallback exists.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Same content as v1.34.24 (v1.104.3-headpin == v1.104.7, retagged per semver
policy: real monotonic patch versions, no suffix tags). go.mod has no replace
directives; removes the leftover comment from an already-completed cascade.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Pins luxfi/evm v1.104.3-headpin: dedicated unbalanced GC pin on the accepted
head state root (transferred head-to-head in Accept + startup + direct
setters) so the head can never be evicted by the tipBuffer/Cap while idle —
the durable fix for the 1085200 'missing trie node at head' freeze. Also
carries vm v1.2.6 parity. Base: v1.34.22 (mainnet matcher lineage), no other
changes.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The COMPLETE mainnet reliability build. v1.35.29 adds FIX#1 (self-finality floor — a
transient bftCommittee count can never self-finalize without quorum; the 1085016 accept-
without-quorum safety hole) + FIX#3 (canonical vote aggregation — sibling wrappers of the
same inner block combine into ONE cert; the block-288 wrapper-split cert-termination stall)
+ the v4 clone-resign inner->outer fix (my v1.35.28 re-sign silently no-op'd on real wrapped
blocks). Matcher EVM v1.104.3-hotfix + Bug B UNCHANGED — execution-identical to mainnet.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Bundles BOTH reliability fixes on the mainnet-compatible line (the live test proved they
COMPOUND — Bug A catch-up alone can't reconcile the tip-loss Bug B causes on restart):
- Bug A (consensus v1.35.28, merged to consensus main): catch-up #1/#2/#3/#5 + vote-guard clone re-sign.
- Bug B (EVM v1.104.3-hotfix): head-state Reference one-liner — EXECUTION-IDENTICAL to mainnet
(golden roots identical RED vs GREEN), matcher EVM (precompile v0.19.0), NO settle-only.
go.mod delta vs mainnet v1.34.14 = consensus-only; EVM = matcher+Reference (the only exec delta is
the proven-identical GC pin). STAGED for mainnet recovery — NOT rolled (gated on RED + canary).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
MAINNET-COMPATIBLE node-only patch: the v1.34.14 line + catch-up/preference fixes + vote-
guard, EVM PLUGIN UNCHANGED (EVM_VERSION=v1.104.3, precompile v0.19.0, chains v1.7.2, vm
v1.2.5 — byte-identical to mainnet; NO settle-only, NO state-transition change). go.mod delta
vs v1.34.14 = consensus ONLY. Consensus v1.35.28 touches vote/cert LIVENESS only (engine is
byte-identical across the lines); it carries #2 (GetBlock-guarded build-tip steer) + the
vote-guard clone-artifact re-sign fallback. Cherry-picked #1/#3/#5 (proposervm validate-
before-assign, EmptyNodeID->real peers, have-block!=not-behind cert-fetch) on the prior commit.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The recurring freeze: a validator that fell behind could never rejoin, spamming
"sentTo=0" and "failed to fetch preferred block" until bounced. Two node-layer
defects, both BFT-safe (compared against ava avalanchego proposervm):
#1 proposervm.SetPreference (vms/proposervm/vm.go): assigned vm.preferred BEFORE
fetching, so a build-tip the node doesn't hold POISONED vm.preferred forever
(BuildBlock errors on every attempt; Quasar cert-finality has no re-converge
poll). Now: getBlock (both post-fork + inner stores) FIRST, delegate to inner,
adopt the preference only on success; on a total miss keep the last held tip
and stay live. Identical to ava in every case its single-store invariant
produces; only adds recovery for Lux's build-tip steering (never bricks).
SetPreference is not an acceptance gate, so safety/agreement is untouched.
#3 peer selection (chains/manager.go): consensus signals "fetch a certified-but-
untracked block" via ids.EmptyNodeID; the old code Add(EmptyNodeID)+Send, so
GetAncestors went to ZERO peers and the cert-verified gap was never fetched.
Now: when nodeID is Empty, sample real connected peers that track this chain's
network (same selection pollFrontierOnce uses); the cert already gated the ask.
Adds vm_rejoin_wedge_test.go. (Defect #2 — the engine build-tip steering in
consensus/engine/chain — is the upstream root cause, tracked separately.)
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The chain-VM plugins are built with '|| echo WARN' (optional) but hard-COPY'd in
the runtime stage (a build miss -> cryptic COPY failure). Replace the single
bridgevm test-s check with a guard over every mandatory plugin so a missing/empty
binary fails loudly at the build stage with the real cause.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The consensus engine's finality object is VotePosition.CanonicalID (the signed
accept vote binds it and EXCLUDES the outer envelope id). It reads it via a
structural canonicalCommitter assertion (engine/chain canonicalIDOf); until now
NO block type implemented it, so canonicalIDOf fell back to the OUTER proposervm
envelope id. On the stuck C-Chain the designated proposer re-wraps the SAME inner
block each slot -> 758 distinct outer ids over ONE inner block 25Q837Lw -> 758
distinct canonicals -> votes scatter -> no alpha-of-K cert -> frozen at 1082879.
postForkCommonComponents now returns the inner block's id as CanonicalID and the
inner parent's id as ParentCanonicalID, so every outer wrapper of one inner block
(and a forked parent) collapses to ONE consensus object. Enables the consensus
v1.35.23/24 canonical-representative determinism fix to actually engage; neither
part alone unfreezes mainnet.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
v1.34.12 crash-looped on boot:
panic: threshold: scheme BLS already registered
consensus <= v1.35.22 blank-imported the OLD crypto/threshold/bls in
quasar.go while node imports the NEW threshold/scheme/bls; both register
BLS into the crypto/threshold registry -> panic. consensus v1.35.24
repoints quasar.go to threshold/scheme/bls.
Verified on the FULL node binary dep closure (go list -deps ./main, 1028
pkgs): luxfi/crypto/threshold/bls = 0, luxfi/threshold/scheme/bls = 1 —
exactly one BLS registration path, the panic cannot recur.
Pins: consensus v1.35.24, chains v1.7.2, evm v1.104.3.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Supersedes the v1.34.11 image build, which failed in the Docker plugin
stage: chains v1.7.1 pulled consensus v1.25.35, whose polaris.go imports
the removed pulsar/ref/go/pkg/pulsar path — the quantumvm + mpcvm plugins
could not build and their hard COPY failed.
- chains v1.7.1 -> v1.7.2 (consensus bumped to v1.35.22 there; consistent
pulsar graph; all 10 chain VM plugins verified building standalone).
- Dockerfile CHAINS_REF v1.7.1 -> v1.7.2.
- consensus v1.35.21 -> v1.35.22 (Pike-clean of the ProposalKey liveness
fix; logic identical to v1.35.21).
Pins: consensus v1.35.22, chains v1.7.2, evm v1.104.3. Off origin/main,
no WIP.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
IMAGE/BUILD change (packaging only — the consensus behavior change ships
separately as luxfi/consensus v1.35.21). Two edits make the deployable
node image build with the fixed consensus and the current chain plugins:
- consensus pin v1.35.20 -> v1.35.21 (ProposalKey decomplection, commit
776a5119c: outer block IDs are transport aliases, proposal identity is
ProposalKey{ParentID,Height} — proposervm envelope churn can no longer
spawn own sibling candidates that split votes; the mainnet 1082880 fix).
- Dockerfile ARG CHAINS_REF v1.7.0 -> v1.7.1. v1.7.0 still shipped the
pre-rename `thresholdvm` dir, so the Dockerfile's `cd /tmp/chains/mpcvm`
plugin build produced nothing (swallowed by `|| echo WARN`) and the hard
COPY of the mpcvm plugin (tGVBwRxp...) failed — the v1.34.10 Docker build
break. v1.7.1 carries the mpcvm rename; the mpcvm plugin builds clean
(verified standalone, 16MB) and all 10 chain plugins are present.
Pins verified for the canary image: consensus v1.35.21 (=776a5119c),
chains v1.7.1, evm v1.104.3, cevm v0.19.0, dex v1.5.15. Built off
origin/main — no WIP from any other worktree.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Pins node to the published tags carrying this session's LP-0130 work:
ids/constants add M/F chain identifiers (drop T-Chain), genesis adds
M/FChainGenesis, chains has the mpcvm rename, consensus has the
mempool-churn pre-build gate + threshold scheme/bls repoint.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
v1.34.9 = v1.34.8 + the C-Chain EVM plugin rebuilt from evm v1.104.3
(= evm v1.104.1 + luxfi/vm v1.2.6). The only change is the plugin's
ZAP RPC server now serializes VM state calls, fixing the concurrent-map
crash (chain.State.verifiedBlocks) that killed the EVM subprocess under a
concurrent ParseBlock storm and froze Lux mainnet C-Chain at block 1082879.
Node binary, consensus, genesis, and all other plugins are unchanged;
block output is byte-identical, so v1.34.9 is consensus-compatible with the
running v1.34.8 fleet (verified on devnet/testnet before the mainnet roll).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The BLS Scheme impl moved from crypto/threshold/bls to
luxfi/threshold/scheme/bls (all threshold impls now live in one module;
the interface stays in crypto/threshold). One-line blank-import path
change; no behavior change.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Pairs the proposervm stale-parent slot-snap churn-fix (f840336167) with consensus
v1.35.17 (view-change abandonment must never silence a height). Together they close the
distributed VC liveness stall that keeps mainnet C-Chain safe-but-paused: slot-snap kills
the stale-parent rebuild sibling explosion at the source (one stable candidate per slot),
and v1.35.17 keeps the round machinery driving the height to convergence instead of going
silent after rePoll's 8-attempt cap. Liveness-only; decided-floor / no-double-finalize
safety invariant untouched. Gated on the live idle-inclusive devnet re-storm + RED before
any testnet/mainnet roll.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Off a stale parent (idle past the proposer window → anyone-can-propose), buildChild
was stamping a raw wall-clock timestamp (Time().Truncate(1s)), so every rebuild of the
same inner block produced a DISTINCT envelope id. That is an unbounded, ever-growing
sibling set at one height: the round-scoped view-change can never gather α aligned
prevotes on a single candidate (no proof-of-lock forms) and finality stalls fleet-wide
(the distributed liveness stall).
Snap the child timestamp DOWN to the parent-anchored proposer-window grid
(parentTimestamp + slot*WindowDuration, slot = TimeToSlot(parent, now)) so all rebuilds
WITHIN one slot are byte-identical → one stable candidate the view-change converges on.
Safe by construction: TimeToSlot(parent, snapped) == slot == TimeToSlot(parent, now), so
the block's slot — hence ExpectedProposer's proposer-window / signed-unsigned verdict and
the derived epoch — is unchanged. Only slot>0 snapped, so a fresh in-window child keeps
its exact timestamp (zero live-chain change), and a snapped time is strictly > parent
(monotonic) and <= now (not advanced). Liveness-only; no safety surface touched.
Extracted as slotSnappedChildTimestamp + unit-proven (idempotence, slot-invariance,
monotonicity). Full proposervm suite green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Two concurrent-map races on the consensus-adjacent validator-set path,
both surfaced by the 5-node storm re-gate, both RED-verified fixed:
1. m.caches concurrent map write (getValidatorSetCache check-then-insert,
reached via proposervm windower.ExpectedProposer across chains during
buildBlocksLocked): guarded with a dedicated cachesLock sync.Mutex
(atomic check-create-store; plain Mutex, no lost-update).
2. TrackedChains concurrent map read/write: getValidatorSetCache read the
network's runtime-mutable cfg.TrackedChains lock-free (the admin
setTrackedChains RPC .Add's it under peersLock). Decoupled via an
immutable startup snapshot (set.Of(cfg.TrackedChains.List()...) in
NewManager); the false 'immutable after startup' comment is corrected.
Snapshot is correct (caching is pure-perf; cache.Empty recomputes),
and the construction clone provably happens-before any writer (P-chain
init is synchronous, admin API serves only post-node.Dispatch()).
Gates multi-validator restart-storm crash-freedom (before validator #2).
-race regression tests reproduce both races pre-fix, clean post-fix
(-count=5). Tracked follow-up (same class, network's live alias, admin-API
default-off, NOT this path): config/internal.go:120-121 + health.go:30 —
fix before ever enabling the admin API.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Harden the bootstrap checkpoint — the one path that bypasses the beacon quorum —
into a cryptographically SIGNED weak-subjectivity anchor.
- Checkpoint carries an authority Signature over a domain-separated canonical
(id,height); CheckpointVerifier (injected, backed by a proven primitive —
Ed25519/BLS, never custom crypto) authenticates it. The policy stays crypto-free
exactly like AncestrySource/heightOf.
- AcceptsFrontier trusts a pinned checkpoint ONLY when the configured authority
signed that exact (id,height). Present-but-unsigned, signed-by-a-non-authority,
signature-transplanted-to-another-(id,height), and no-verifier-wired all FAIL
CLOSED. A compromised flag cannot inject a false sync anchor without also forging
the authority's signature. It is a cryptographic vouch, NEVER a ⅔-live-stake tally.
- No production path pins a checkpoint yet (opt-in operator escape hatch), so the
invariant is enforced for when it is activated; nothing to wire now.
Folds consensus v1.35.16 (single ⅔ formula + leaf-lock doctrine) into the node.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>