Two node/bootstrap-layer fixes for a clean rolling validator upgrade. Consensus
engine untouched (v1.35.5 boot-seed from v1.32.10 carried forward).
[HIGH — correctness] info.isBootstrapped(C) tracks REAL state, not premature true.
manager.IsBootstrapped(id) returned true the instant a chain merely EXISTED in
m.chains (set right after createChain launched the async, possibly-stalling
bootstrap goroutine) — so a C-Chain stalled at genesis (head 0x0) reported
info.isBootstrapped(C)=true, masking the stall from any wait-for-healthy gate.
Now keys on the SAME sb.Bootstrapped signal the readiness health check uses
(m.Nets.IsChainBootstrapped), set only after runInitialSync reaches the named
frontier and the VM goes to normal operation (head advanced, eth-RPC live).
GetChains().Bootstrapped fixed identically. New per-chain query on nets.Net +
chains.Nets (nil-safe). Regression test TestIsBootstrappedTracksRealConvergence.
[MEDIUM — robustness] WIPE-path GetAncestors fetch hardened for ≥2 peers at genesis.
Applying the proven avalanchego contract (studied in snowman/bootstrap +
getter): (a) Ancestors buffers the whole sample and SKIPS empty batches,
returning the first NON-EMPTY one — a size-1 channel let a fast empty reply from
a genesis peer win the race and starve a peer that actually held the ancestry;
(b) sampleAncestorBeacons PREFERS beacons the frontier round found genuinely
ahead (they hold the ancestry) — ava's PeerTracker "ask a prover" bias sourced
from the replies we already have, no separate tracker. Tests:
TestNodeBootstrap_WipePath_MixedGenesisPeers_ObtainsAncestry,
TestSampleAncestorBeacons_PrefersAheadBeacons.
Build: main+chains+nets+service/info compile CGO_ENABLED=0 (ARC/Dockerfile path);
full chains+nets suites green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
External (HTTP / JSON-RPC) is the only place JSON is legitimate. Every
existing encoding/json import in node/ moves to github.com/go-json-experiment/json
(v2 root, not the v1 sub-package). NewEncoder/NewDecoder rewrite to
MarshalWrite/UnmarshalRead. MarshalIndent rewrites to Marshal with
jsontext.WithIndent. json.RawMessage rewrites to jsontext.Value.
*json.SyntaxError rewrites to *jsontext.SyntacticError.
81 files migrated. LLM.md captures the rule + v1->v2 delta table.
Known v2 semantic deltas surfaced by existing tests (followups, not regressions):
- [N]byte fields with no MarshalJSON now marshal as base64 string (v1 marshalled
as JSON array of byte numbers). Affects vms/platformvm/txs/*_test.go fixtures
with embedded BLS proofOfPossession.
- time.Duration has no v2 default representation; configs that wire-format
Duration as nanoseconds (vms/{xvm,platformvm}/config, config/spec) need to
switch to string-form Duration or carry an explicit option. v2 root does not
re-export FormatDurationAsNano.
- v2 enforces strict UTF-8 (vms/chainadapter/messaging fixture has non-UTF-8).
- json.MarshalWrite does not append a trailing '\n' (v1 NewEncoder.Encode did);
service/auth/auth_test.go expectation updated.
- nil []byte round-trips to empty (not nil); config_test deep-equal fixtures
surface this.
All affected sites are at the API boundary; ZAP wire envelope already covers
the internal data paths (state, P2P, consensus, MPC, threshold). Internal
JSON sites that should move to ZAP next (separate work):
- vms/da/store.go (DA blob/cert storage as JSON)
- vms/platformvm/airdrop (airdrop claims as JSON in db)
- vms/chainadapter/appchain (SQLite materializer schema/data blobs)
- vms/chainadapter/messaging (conversation codec)
- staking/kms.go (KMS HTTP client — external technically, leave)
- utils/{bimap,ips} (small marshaler shims — low priority)
v1.30.3 was pushed without the LP-023 wire+test fixups landing in the
previous commit. v1.30.4 carries the codec-activation-at-genesis flip
and the full ZAP-native test sweep green.
vms/proposervm/block/parse_test.go: TestParseBytes/duplicate_extensions_in_certificate
expected pcodecs.ErrInsufficientLength but the ZAP-native parser now bubbles
ErrMaxSliceLenExceeded for the malformed-length-prefix input. Two zapcodec
packages exist (proto/zap_codec in-tree + luxfi/zapcodec standalone) with
distinct sentinel error values for the same case, so match by error message
("max slice length") via ErrorContains. Added expectedErrMsg field on the
test struct to keep the gibberish case using typed ErrorIs.
version/constants.go: defaultMinor 28→30, defaultPatch 0→2. Without ldflags
the binary was reporting luxd/1.28.0 even though latest tag is v1.30.1.
v1.30.2 is the rollout candidate this becomes after CI tags.
Closes the codec-version trap that surfaced when the v1.23.x ("Apricot/Banff") tx + block layout was rip-replaced in 409297a089 without bumping the wire-version prefix: mainnet/testnet on-disk blobs (~1.08M+ C-Chain blocks) lost a decoder, and any code path that round-tripped a tx through tx.Initialize re-marshaled it under the new layout — rotating TxID and breaking chain-commitment continuity.
Strategy A per cryptographer / orchestrator brief:
* Register both layouts on the platformvm tx + block codec.Managers under
distinct wire-version prefixes:
- CodecVersionV0=0 = v1.23.x slot map (TransferInput=5, hole=6, ...,
AddPermissionlessValidator=25, ..., DisableL1Validator=39)
- CodecVersionV1=1 = current slot map (with MintOutput/MintOp at 6/8,
+4-skip for Banff txs at 27-30, CreateSovereignL1Tx at 36,
SlashValidatorTx at 41, CreateAssetTx/OperationTx at 42-43)
- txs.Codec / block.Codec dispatch by the standard 2-byte wire prefix.
* New byte-preserving init: tx.InitializeFromBytes(c, version, signedBytes)
and tx.InitializeFromBytesAtVersion(c, version) bind the tx to its
original signedBytes without re-marshalling. tx.Initialize stays as the
fresh-build path; from-DB / from-wire paths route through the
byte-preserving variant. TxID = hash(signedBytes) under the version it
was written at, forever.
* New vms/platformvm/block/v0/ subpackage: 9 v0-only block kinds
(ApricotProposalBlock, BanffProposalBlock, ... at slots 0-4 + 29-32).
Pure DTOs — no codec, no Visit. The block package wraps the decoded
v0.Block in a liftedV0Block adapter that:
- returns the original bytes verbatim (no re-marshal),
- BlockID = hash(raw v0 bytes),
- dispatches Visit to the v1 Visitor arms (ApricotProposalBlock /
BanffProposalBlock -> ProposalBlock, etc.),
- re-binds embedded txs at v0 via InitializeFromBytesAtVersion so
inner TxIDs are also byte-preserved.
* genesis.Parse is wire-version-aware: pre-codec-v1 genesis blobs decode
at v0, new blobs at v1. The matching codec is used for tx re-binding.
* L1-tx slot map shifts +1 to accommodate CreateSovereignL1Tx at 36
(RegisterL1Validator 36->37, SetL1ValidatorWeight 37->38,
IncreaseL1ValidatorBalance 38->39, DisableL1Validator 39->40). Test
fixtures regenerated.
* 22 fee-calculator fixtures + 11 serialization fixtures bumped to use
the v1 wire prefix (0x0001) and the post-CreateSovereignL1Tx slot map.
Migration notes:
* Mainnet + testnet P-Chain DBs: NO rebuild. Pre-codec-v1 blocks
continue to decode through the v0 path with original BlockID and
TxIDs preserved. New blocks are written at v1 from the cut-over
height onward.
* Devnet: must be rebuilt before rolling to v1.28.0. Its existing
blobs carry wire-version 0 but use the post-rip slot map (not the
v0 Apricot/Banff layout) — decoding them through the v0 path would
read the wrong types. A fresh bootstrap at v1.28.0 writes v1 bytes
from height 0 and is internally consistent thereafter.
Tests:
* TestCodecVersionV0V1Coexist, TestParseDispatchesByVersion,
TestTxIDStabilityRoundTrip, TestCrossVersionRefuses (txs)
* TestParseV0ApricotProposalBlock, TestParseV0BanffStandardBlock,
TestParseV1RoundTrip, TestVersionPrefixDispatch (block)
* 150 packages, 0 failures under -race
Carries the namespace rename (lux→security) on /ext/security and the
F118 chain-manager profile-pin stamping landed in v1.26.12. Patch bump
only — no schema or behavior change beyond the rename of the
JSON-RPC namespace and REST sidecar path on the security service.
compatibility.json: add v1.26.12 + v1.26.13 to RPCChainVMProtocol 42
so TestCurrentRPCChainVMCompatible passes (the previous bump to
v1.26.12 left the map at v1.23.25).
Companion to coreth: c84950de4. The chain manager now forwards the
chain-wide ChainSecurityProfile resolved at node bootstrap (F102)
into the C-Chain (coreth) plugin Initialize payload by stamping a
profileID + profileHashHex pin into the JSON config bytes that
already cross the rpcchainvm boundary.
- chains/manager.go adds ManagerConfig.SecurityProfile and an
injectSecurityProfileConfig pass (mirror of injectAutominingConfig)
that runs only on EVMID and is a no-op when the profile is nil.
- node/node.go threads n.securityProfile into chains.ManagerConfig.
- version/constants.go patch-bumps to 1.26.12.
Four F118 tests in chains/security_profile_f118_test.go:
- StampsCChainOnly — pin appears in EVMID config, not in other VMs.
- NoOpWhenProfileNil — classical-compat path passes through.
- MergesExistingConfig — automining + skip-block-fee survive.
- RoundTripsAcrossPluginBoundary — the wire form decodes cleanly
into the coreth-side LuxSecurityProfilePin struct shape, with the
hash decoding to a full 48 bytes (SHA3-384 width).
go.mod bumps consensus v1.23.5 → v1.23.9 to pick up the F113
checkpoint+Merkle SHA3-384 widening.