fix(network): bind strict-PQ peer identity to staking ML-DSA key so validators produce blocks#131
Closed
Darkhorse7stars wants to merge 226 commits into
Closed
fix(network): bind strict-PQ peer identity to staking ML-DSA key so validators produce blocks#131Darkhorse7stars wants to merge 226 commits into
Darkhorse7stars wants to merge 226 commits into
Conversation
added 30 commits
April 13, 2026 03:45
The minimum quantum-safe validator set is: P — staking, validators, rewards (implicit) Q — Quasar PQ consensus (BLS + Ringtail + ML-DSA) Z — universal receipt registry + ZK verification X — assets (kept for LUX token / fee UTXOs, backward compat) Opt-in (only created if *ChainGenesis provided): C — EVM contracts D — DEX B — Bridge T — Threshold/FHE/MPC Validators stake extra + validate opt-in chains to earn their fees. Fee split: each chain's tx fees distributed to its validators proportional to stake weight.
Config validation, quorum params, lifecycle, set/get finalized, BLS signature types, RingtailCoordinator sign/verify paths, active/inactive validator weight filtering. Remaining uncovered: GPU/NTT hardware code (requires CGO + GPU), processFinality integration (requires P-Chain provider), Verify (requires real BLS/Ringtail key material).
Consistent naming — package matches directory name: - bvm → bridgevm - gvm → graphvm - qvm → quantumvm - tvm → thresholdvm - zvm → zkvm Removed VMs (deduped): - teleportvm/ — duplicated bridgevm+relayvm+oraclevm code (same MPC, same signing) - servicenodevm/ — moved to dedicated repo github.com/luxfi/session vms.go: 11 optional VMs (A/B/D/G/I/K/O/Q/R/T/Z) registered with new package names. S-Chain (Session) registered separately as standalone plugin.
…vm,oraclevm,quantumvm,relayvm,thresholdvm,zkvm} VM implementations moved to github.com/luxfi/chains/<name>. node/node/vms.go now imports from chains/ paths. node/vms/ retains only primary network VMs (platformvm, xvm, evm) plus VM infrastructure (manager, registry, rpcchainvm, tracedvm, etc).
One place, one way to define UTXO primitives. Delete the in-tree duplicate at node/vms/components/lux/ and import the standalone luxfi/utxo package with a 'lux' alias so existing call-sites (lux.UTXO, lux.TransferableInput, etc.) remain unchanged in consumers. Files changed: 167 imports rewritten, 2 directories deleted. Build still green on the whole tree except pre-existing examples/multi-network QChainMainnetID issue (unrelated). Next: rename luxfi/utxo/luxmock → utxomock for full brand-neutrality; that is a separate PR since it requires touching all consumer alias names.
Restores the missing entry point that .goreleaser.yml, Makefile, and scripts/build.sh all expect at ./main. Without this file, the release pipeline fails with 'couldn't find main file: stat main: no such file or directory'. The main wires: config.BuildFlagSet → config.BuildViper → config.GetNodeConfig → log.NewFactoryWithConfig → ulimit.Set → node.New(*node.Config, log.Factory, log.Logger) → n.Dispatch() with SIGINT/SIGTERM handling → exit n.ExitCode() --version short-circuits to print version.CurrentApp.String() (e.g. 'luxd/1.23.25'). Not using the orphan app/ package (app.New takes nodeconfig.Config which is a parallel type definition not produced by config.GetNodeConfig). The app/ package can be migrated separately or removed.
Goreleaser runs with GOWORK=off so it cannot resolve local workspace
modules. Pin published versions:
- github.com/luxfi/utxo v0.2.7 (adds InitRuntime forwarder)
- github.com/luxfi/chains/{aivm,bridgevm,dexvm,graphvm,identityvm,
keyvm,oraclevm,quantumvm,relayvm,thresholdvm,zkvm} v0.1.0
No replace directives — every dependency resolves to a published tag.
Single ghcr.io/luxfi/node image now includes luxd + EVM plugin + all 11 chain VM plugins (aivm, bridgevm, dexvm, graphvm, identityvm, keyvm, oraclevm, quantumvm, relayvm, thresholdvm, zkvm) built from github.com/luxfi/chains. Dockerfile: added plugin-builder stage that clones luxfi/chains and builds each VM as a static binary placed at the CB58-encoded VM ID path under /luxd/build/plugins/. compose.yml: single-node local dev config (network-id=3, sybil off, sample=1) replacing the stale docker/compose.yml. k8s: replaced ad-hoc mainnet-only manifests with kustomize base + 4 overlays (localnet, devnet, testnet, mainnet). Each overlay patches replicas, resources, storage class, network-id, and image tag. Deleted: docker/Dockerfile, docker/compose.yml, docker/compose.genesis.yml, k8s/luxd-statefulset.yaml, k8s/mainnet/ (entire directory).
Blocking (would cause production incidents): - env var prefix: LUX_* → LUXD_* (27 vars). Viper reads only LUXD_*; LUX_* silently ignored → node would start with default config on every env. - admin API enabled + sybil disabled defaults: explicit LUXD_API_ADMIN_ENABLED=false and LUXD_SYBIL_PROTECTION_ENABLED=true in base ConfigMap. - bootstrap peer config absent in new overlays: added LUXD_BOOTSTRAP_IPS / LUXD_BOOTSTRAP_IDS keys to base ConfigMap; overlays/operator populate per-env. Defense-in-depth additions: - ServiceAccount `luxd` with automountServiceAccountToken: false (no API access). - PodDisruptionBudget maxUnavailable=1 (works at 5 → 100 validator scales). - NetworkPolicy: ingress 9631/TCP from any (P2P), 9630/TCP cluster-only (HTTP), 9090/TCP monitoring-namespace only (metrics). - podAntiAffinity preferredDuringScheduling by hostname — spread across nodes. - updateStrategy: OnDelete — operator drains one pod at a time. - podManagementPolicy: Parallel — validators start in any order. Swarm verdict was NO-GO until these fixes. Addresses red critical #1 (admin API), #2 (sybil), high #4 (gateway bypass via direct LB), medium #7 (no NetworkPolicy/RBAC), and scientist blockers on env prefix + bootstrap.
… builder Remove unused bloom filter, test helpers, metrics scaffolding, and linearizable VM wrapper. Trim stale go.sum entries. Add genesis builder helpers and xvm FX/genesis initialization.
…icKey []byte signature; service/info Consensus surfacing
…perator owns ARC)
Lux unifies subnet (validator-set) and chain identity into a single
ChainID — every chain identifies itself by its ChainID, no separate
'subnet identifier' needed.
Wire/interface changes (BREAKING — coordinated rollout):
- node/message/wire: ChainSubnetPair struct → ChainPingEntry; collapsed
duplicate SubnetId field; ChainSubnetPairs → ChainIds
- node/proto/zap/p2p: SubnetID → ChainID
- node/vms/chainadapter: ICPBlock/ICPSubnet SubnetID → ChainID
- validators/uptime: Calculator interface params subnetID → chainID
- consensus/core/router: Connected method param subnetID → chainID
- p2p/message: outbound_msg_builder var renames
Config/UI changes (cleanup):
- genesis/pkg/genesis ChainEntry: dropped redundant SubnetID field
- tui/views/ChainStatus: dropped redundant SubnetID field
- cli: comment cleanup
- benchmarks: deploy-subnets var rename
Lux's primary wire protocol is ZAP (Zero-Copy App Proto). The protobuf
definitions and generated .pb.go files in proto/ trees were vestigial:
- _grpc.pb.go gRPC service stubs were already gated behind
//go:build grpc and not part of the default build
- .pb.go message types had ZAP equivalents (*_zap.go) on every active
code path
Default build was already 100% ZAP. Deleting the .proto + .pb.go +
_grpc.pb.go files removes dead code without functional change.
Verified clean build of node, p2p, vm after deletion.
Introduces ZWingDialer + ZWingListener which wrap any underlying
net.Conn (TCP, hostname, RNS mesh link, Unix socket, in-memory pipe)
with the canonical Lux PQ secure channel:
IETF X-Wing KEM (X25519 + ML-KEM-768)
Hybrid Ed25519 + ML-DSA-65 identity, signed transcript
ChaCha20-Poly1305 with sequence-numbered nonces
Z-Wing's contract is "any net.Conn" — the same secure channel rides
unchanged on TCP today and on the existing RNS transport tomorrow
without a per-transport rewrite. The legacy LP-9701 in-RNS-link crypto
(rns_link.go) stays in place during the transition; new p2p paths
should layer ZWingDialer over the EndpointDialer instead of relying on
LP-9701's inline encryption.
Adds:
network/dialer/zwing_dialer.go ZWingDialer + ZWingListener
network/dialer/zwing_dialer_test.go 5 e2e tests covering:
- missing-identity rejection (dialer + listener)
- real TCP listener + Z-Wing handshake + payload round trip
- Wrap() over an arbitrary net.Conn (net.Pipe stand-in for RNS)
- identity mismatch (MitM defence)
- DialEndpoint over an Endpoint (works for IP, hostname, future RNS)
Bumps:
github.com/luxfi/zwing v0.5.2 (full FIPS 204 PQ stack, cross-language
wire-byte interop with Rust/Py/TS)
github.com/luxfi/api v1.0.10 (NewListener seam used by zwing.ListenZAP)
luxd-side wiring (node.go construction with a loaded LocalIdentity) is
the next follow-up; this commit lands the seam without behavioural
changes to the existing dialer interface.
Pulls in:
* luxfi/constants v1.5.2 — LocalID/CustomID semantic split, IsCustom()
* luxfi/genesis v1.9.2 — same split mirrored at the configs layer
* luxfi/zwing v0.5.2 — full PQ secure channel (X-Wing + ML-DSA-65 +
ChaCha20-Poly1305) with cross-language
wire-byte interop verified against Rust /
Python / TypeScript ports
* luxfi/api v1.0.10 — zap.NewListener seam (used by zwing.ListenZAP)
* luxfi/netrunner v1.18.1 — PQ-mandatory zapwire control RPC + same
LocalID rename
* luxfi/geth v1.16.87 — verkle.Fr type fix (drop bandersnatch import)
* luxfi/consensus v1.23.1 — banderwagon path move tracked
Source-side rename in this repo: every constants.CustomID call was
"the local 1337 dev network", so they all become constants.LocalID
(both the upgrade-config validation switch and the test fixtures).
The old constants.CustomID literal is now 0 — used as the explicit
"this is a user-defined custom network" sentinel, separate from the
LocalID dev network.
Plus a clarifying comment on the upgrade-config switch noting which
network IDs are permitted to override their upgrade schedule (any
non-well-known ID, including DevnetID/UnitTestID and any genuinely
custom user-defined network ID).
Z-Wing dialer + tests still green; all network/dialer, config,
config/node, config/spec, and genesis/builder tests pass.
* consensus v1.23.2 — pulls threshold v1.6.7 with the LSS-Pulsar
adapter (PulsarConfig, DynamicResharePulsar) needed by the epoch
manager's resharing path.
* New proto/zap/rpcdb package — pure-Go wire types for the database
RPC, no protobuf. Mirrors the proto/zap/{vm,p2p,sync,platformvm}
pattern.
* New proto/rpcdb/rpcdb_zap.go (build tag !grpc) — full ZAP-native
DatabaseClient + DatabaseServer + Register hook, implements
database.Database against any Transport. The host's underlying DB
is luxfi/database (zapdb in production); the protocol shape is
identical regardless of which engine the host runs.
* Renamed the existing rpcdb.go to rpcdb_grpc.go so the two
variants are mutually exclusive: build with no tag (ZAP default)
or with `-tags=grpc` (protobuf path), never both.
examples/multi-network/multi-network-poc.go: drop duplicate-key map
entry where Q-Chain was illustrated as a separate network — Q-Chain
shares the primary network ID per LP-134, see comments on
MainnetID entry.
Net effect: full default build is ZAP-only PQ-secure-channel-aware,
RPC over a Z-Wing-encrypted ZAP transport, no gRPC/protobuf in the
critical path.
Two compatibility seams that downstream consumers need post-Z-Wing restructure: 1. proto/pb/* stubs (build tag grpc) — every grpc-tagged file under proto/<name>/<name>_grpc.go (and the legacy proto/rpcdb/rpcdb_grpc.go) imports proto/pb/<name>. Those dirs were empty (git ignores empty dirs), so consumers' `go mod tidy` walked the imports under the grpc tag and failed to resolve the package. Default builds use ZAP and never enter these stubs; the grpc-tag path now compiles cleanly even though the protobuf types themselves still need regeneration when someone actually wants the gRPC transport. 2. vms/dexvm/dexvm.go — re-export shim for the canonical chains/dexvm package. The DEX VM moved out of node/vms/dexvm into github.com/luxfi/chains/dexvm; existing callers (~/work/liquidity/ node, etc.) keep building unchanged. Default build clean; grpc-tagged build also resolves. No on-the-wire behaviour change in the ZAP default path.
* vms/components/lux: restore the X-Chain UTXO type tree from v1.24.29. External consumers (~/work/liquidity/network-bootstrap, PlatformVM tx builders, AVM tx builders) import this exact type tree to interop with the X→P export path. Documented in CLAUDE.md as a known anomaly pending #58 consolidation; until that lands the package must be present. * vms/thresholdvm/thresholdvm.go: re-export shim mirroring vms/dexvm. The Threshold (FHE / MPC) VM moved out of node/vms/thresholdvm into github.com/luxfi/chains/thresholdvm. Callers (liquidity/fhe plugin etc) keep building unchanged. No on-the-wire behaviour change.
Anchor /lux + /luxd binary patterns at repo root so the gitignore rule doesn't accidentally match every directory called "lux" deep in the tree (which was excluding vms/components/lux that the previous commit tried to land). Add the actual UTXO type tree files so v1.26.5 ships with the package contents external consumers (liquidity/network-bootstrap etc) need.
X-Chain genesis at builder.go:603-613 references 9 FxIDs (secp256k1fx, nftfx, propertyfx, mldsafx, slhdsafx, ed25519fx, secp256r1fx, schnorrfx, bls12381fx) but chains/manager.go only registered the first 3. Chain init failed with "fx qC5JEjDhfXD66cGuhtiL3Lkka2SgZyht74nHVQFmYFyDiLqXe not found" — that ID encodes 'mldsafx' which the genesis builder added when post-quantum support landed but wasn't paired with a manager-side registration. Add all 6 missing factories so any FxID emitted by the genesis builder loads at chain create time. The fxs map is now structurally identical to the X-Chain FxIDs slice — drift between the two will become a review-time diff in chains/manager.go vs. genesis/builder/builder.go.
The field has always semantically been the X-Chain asset ID — the JSON tag is already `xAssetID`. The Go name `LuxAssetID` was a holdover from when "LUX" was the only asset name on the chain, which created confusion when readers expected `XAssetID` to mirror `XChainID` / `CChainID` / `DChainID` (all of which already used the chain-letter naming). Renamed across config, node, vms/platformvm, vms/rpcchainvm/zap and the static_service API request types. JSON tag and protobuf wire fields are unchanged, so on-disk configs and inter-process plugin RPC are backwards-compatible — only Go callers see the new name.
The struct `zapwire.InitializeRequest.LuxAssetID` is generated from a .proto in luxfi/api — renaming it requires regenerating the wire bindings, bumping luxfi/api, then bumping luxfi/node to that. Out of scope for this rename pass; revert the one Go-side assignment so the build resolves against the existing wire type. The local variable already reads from `Context().XAssetID` so the data path is consistent; only the on-the-wire field name lags.
Picks up the localnet genesis regen from LIGHT_MNEMONIC. lux/node's default genesis for network-id=1337 now funds the same 100 P/X wallets the lux/node + liquidityio/node toolchains derive against, so local dev no longer has to set --genesis-file or hand-edit allocations.
…ent) — byte-preserving TxID/BlockID across migration (#123) Closes the codec-version trap that surfaced when the v1.23.x ("Apricot/Banff") tx + block layout was rip-replaced in b1e265a 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
v1.28.0's block-codec multi-version dispatch did not extend to the
P-Chain genesis decoder. genesis.Codec aliased block.GenesisCodec,
which registers only the v1 tx slot map; v0-prefixed cached-genesis
blobs (carried over from v1.23.x bootstraps) errored at first byte
with codec.ErrUnknownVersion.
Root cause hot path: config.getGenesisData -> resolveXAssetID ->
genesis/builder.XAssetIDFromGenesisBytes -> platformvm/genesis.Parse
-> Codec.Unmarshal(bytes, *Genesis). Codec was the v1-only
block.GenesisCodec.
Fix: alias genesis.Codec to txs.GenesisCodec, which registers BOTH the
v0 (Apricot/Banff) and v1 (current) tx slot maps. The Genesis struct
has no slot ID of its own; all version-sensitive data lives in the
embedded []*txs.Tx, so txs.GenesisCodec dispatches the same wire-
prefix lookup the rest of the platformvm tree already uses.
Marshal at CodecVersion (v1) is byte-equivalent because the v1 slot
map in txs.GenesisCodec is the SAME registerV1TxTypes() invocation
block.GenesisCodec was using.
Audit: every other Unmarshal site in vms/platformvm/ that touches
historical wire bytes either (a) goes through block.Parse / txs.Parse
which already dispatch on the prefix, or (b) reads internal state
written by v1-only code (block.GenesisCodec is correct there).
Regression guards in parse_v0_test.go:
- TestParseAcceptsV0CachedGenesis: the canary failure mode.
- TestParseAcceptsV1Genesis: canonical write path still parses.
- TestParseV0RoundtripIsBytePreserving: v0 -> Parse -> re-marshal
-> byte-equal, locking in the doc claim that genesis-derived
hashes do not rotate across the migration.
- TestParseRejectsUnknownVersion: prefixes outside {v0, v1} still
surface as errors.
Full vms/platformvm/... tree green under -race; genesis/builder and
config trees green.
…i-version dispatcher
Closes the residual v1.28.1 testnet-canary failure where bootstrapping
a v1.23.x-written P-Chain database hit:
P-Chain state corrupt after init — database must be wiped
error="loadMetadata: feeState: unknown codec version"
chainID=11111111111111111111111111111111P
The v1.28.1 patch routed genesis.Parse through the multi-version
txs.GenesisCodec dispatcher but did not touch the 7 OTHER state-side
sites that read via block.GenesisCodec — which carried only the v1 slot
map. Any pre-codec-v1 row on disk (feeState, heightRange, L1Validator,
fx.Owner, NetToL1Conversion, legacy stateBlk) errored at the very first
byte with codec.ErrUnknownVersion.
Architecture (Rich-Hickey-simple): make the codec itself complete for
all encountered wire versions rather than asking "which codec does
this caller need". block.GenesisCodec now registers BOTH the v0
(v1.23.x Apricot/Banff) and v1 (current) tx slot maps — reads
dispatch on the 2-byte wire prefix, writes still target
CodecVersion (== v1) exclusively. Same shape as txs.GenesisCodec
(decomplected from block parsing — block.Parse continues to extract
prefix explicitly because v0 blocks satisfy v0.Block, not block.Block,
and cannot be unmarshalled into a block.Block destination).
Audit found 7 state-side sites all using block.GenesisCodec; all 7
are routed through a new defensive helper:
state.multiVersionUnmarshal(c codec.Manager, b []byte, dest any)
The helper is a pass-through to c.Unmarshal but probes the codec on
first observation. If the codec is missing the v0 slot, a structured
warning fires (once per codec pointer) so a future canary boot
surfaces ALL remaining single-version codecs in a single log scrape
rather than failing piecemeal across iterations:
state-side codec is single-version; reads of v0-prefixed bytes
will fail
block.RegisterGenesisType now symmetrically registers on both the v0
and v1 underlying linearcodecs so state-side types (currently:
stateBlk) keep slot-stable shapes across codec.Manager dispatch.
Audit table (all 7 broken sites → fixed):
state/l1_validator.go:222 getL1Validator
state/state_blocks.go:115 parseStoredBlock (legacy stateBlk)
state/state_chains.go:66 GetNetOwner (fx.Owner)
state/state_chains.go:116 GetNetToL1Conversion
state/state_metadata.go:176 loadMetadata (heightRange)
state/state_metadata.go:273 getFeeState <-- canary failure
state/state_validators.go:239 loadActiveL1Validators
state/state_validators.go:535 initValidatorSets (inactive)
MetadataCodec was already multi-version (no fix needed).
txs.GenesisCodec was already multi-version (v1.28.0).
block.Codec stays v1-only by design (block.Block interface destination
cannot accept v0.Block types; Parse handles version split explicitly).
Tests (all -race green):
block/codec_multiversion_test.go 6 tests
state/state_v0_codec_test.go 9 tests including
- TestStateV0FeeStateReadable
(exact canary fixture)
- TestStateBootFromV0SingletonDB
(end-to-end boot simulation)
state/codec_helpers_test.go 5 tests (warning probe +
idempotency + non-blocking
+ multi-version invariant)
-> 20 new regression tests
-> 27/27 platformvm packages green under -race
…c v1.5.7
Pulls in:
- pulsar canonical wire codec (PULS/PULG) + shared lattice/v7/gpu surface
- threshold protocols/{corona,pulsar} alias surface (no direct primitive imports)
- consensus routed through threshold/protocols alias
- metric noop counter race fix (atomic.Uint64)
Cross-repo audit closed: corona+pulsar+threshold production-ready for
permissionless mainnet. End-to-end TestPulsar_Wire_FIPS204Verifiable
asserts threshold-combined signatures are byte-identical to single-party
FIPS 204 ML-DSA signatures (verified externally via cloudflare/circl).
Adds a KMS_ADDR-gated production path between the MNEMONIC env var (priority 1) and the on-disk key files (priority 3). When set, the mnemonic is fetched via luxfi/kms zapclient.LoadMnemonicFromKMS — the same canonical loader every Lux-derived service (luxd, netrunner, lux/cli, descending-L1 bootstraps) now shares. New priority chain: 1. MNEMONIC env var 2. KMS_ADDR + KMS_ENV + KMS_MNEMONIC_PATH (native ZAP, default path /mnemonic) 3. Key name from os.Args[1] (~/.lux/keys/<name>/) 4. ~/.lux/keys/default/ Production env contract matches the Liquidity operator's render (KMS_ADDR + KMS_ORG + KMS_ENV + KMS_MNEMONIC_PATH); every Lux chain inherits the same scheme. Dep: + github.com/luxfi/kms v1.9.12 (carries zapclient.LoadMnemonic) Build clean.
Wire-format codec IDs unchanged. CLI aliases deleted; chain is the command. No backwards-compat shims, no deprecation comments.
Wire-format codec IDs unchanged. CLI aliases deleted; chain is the command. No backwards-compat shims, no deprecation comments.
…clean threshold + MAGS/MAGG wire)
Track A finish: KMS goes back to being a generic secret store; mnemonic semantics live in luxfi/keys alongside the existing BIP-39 + BIP44 derivation. Imports flip from luxfi/kms/pkg/zapclient.LoadMnemonicFromKMS to keys.LoadMnemonicFromKMS — same signature, same behavior. Deps: luxfi/keys v1.0.8 → v1.0.9 (carries the new LoadMnemonic helper) luxfi/kms v1.9.12 → v1.9.13 (LoadMnemonic removed, secret store only) Build clean.
…patcher live, naming decomplect)
…ashLooping The cached `<dataDir>/genesis.bytes` file is written on first start to hold hash stability across restarts. On a binary upgrade that adds new codec types (multi-version v0+v1 dispatcher, etc.), the old cached blob may carry type IDs the new binary doesn't recognise. The existing code surfaced this as `resolve X-Chain asset ID from cached genesis: unmarshal interface: unknown type ID N` and returned an error — wedging the node in CrashLoop with no automatic recovery. Drop the cache and rebuild from the `--genesis-file` instead. Hash stability is forfeit for that single restart (intentional — the alternative is a permanent outage on every binary bump that changes codec types). Subsequent restarts re-establish stability against the fresh cache. Surfaced today on Liquidity testnet+mainnet bumping lqd v1.9.x → v1.10.8: every pod hit "unknown type ID 29" on the stale v1.9.x codec cache and CrashLoopBackOff'd.
fix(config): invalidate cached genesis on parse failure
… cert wired, TEE extensions live)
The KMS consensus-auth gate now requires every secret-opcode envelope to carry a signed identity. Derive a bootstrap ServiceIdentity from KMS_BOOTSTRAP_MNEMONIC (or MNEMONIC) under the well-known servicePath "luxd/staking-bootstrap" and thread it into the LoadMnemonicFromKMS call so the dial envelope is signed. Bootstrap mnemonic is provisioned out-of-band (sealed envelope, HW token unwrap, etc.); the operational staking material on disk still comes from the KMS-held mnemonic the dial then fetches.
…ade) consensus v1.25.8 carries threshold v1.9.1 which carries magnetar v1.1.0: strict-atom Combine, audit-grep clean, byte-identity to circl FIPS 205, 35-51% faster than v1.0. Race-clean across the full node ./... suite (exit 0; no DATA RACE / panic markers; 30+ min compile-and-test on -race -timeout=15m).
luxfi/evm v0.18.15 ships core/genesis: honor SkipPostMergeFields flag from JSON — the fix for the "triedb parent [0x56e81f17…] layer missing" panic-in-eth.New that's blocking C-Chain bootstrap on lux-mainnet. Lux mainnet C-Chain canonical genesis hash is 0x3f4fa2a0…, produced with the 16-field pre-Shanghai header format. The chain activates Cancun at genesis time for MCOPY etc., but the genesis BLOCK itself must stay in the legacy header shape. The previous luxfi/evm tag ignored skipPostMergeFields=true and shifted the computed genesis hash to 0x1ade42ec…, which then failed to commit to pathdb because the parent layer (0x56e81f17… = empty root) wasn't in the layertree. The plugin baked into this image is at vmId mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6 — confirmed via strings of the running prod plugin binary (luxfi/evm/core symbols).
consensus v1.25.8 (carries threshold v1.9.1 + magnetar v1.1.0) refactored
quasar/corona_gob.go and polaris.go to call sig.MarshalBinary() at the
Signature level. The wire-codec methods (Signature.{Marshal,Unmarshal}Binary)
were added in corona v0.7.6 — v0.7.5 only has them on the inner C/Z/Delta
polynomial fields.
The historical replace directive (455b994 on 2026-05-24) was added to
work around consensus v1.24.6 reaching back into corona via keyera.Bootstrap,
which since shipped its 3-value return at corona v0.7.5. consensus v1.25.x
now pins corona v0.7.6 in its own go.mod cleanly, so the replace is no
longer needed and is actively breaking the v1.28.8 image build.
Removing the replace lets MVS pick corona v0.7.6 transitively through
consensus → that is the version where MarshalBinary lives.
Reproduced the CI failure locally with CGO_ENABLED=0 GOWORK=off, fixed,
verified with a clean amd64 nattraversal-profile build (46.6MB binary)
and a full race-clean ./... suite (exit 0, no DATA RACE / panic markers).
luxfi/keys v1.1.0 lands the Bindel-Brendel-Fischlin (CCS 2021) + CDFFJ23 (Asiacrypt 2023) stronger-binding hybrid signature scheme for validator identity: HybridPublicKey / HybridPrivateKey / HybridSignature HybridSign / HybridVerify / HybridBoundDigest / HybridPublicKeyBytes DeriveHybridIdentity (mnemonic + path → HybridIdentity) Construction binds BOTH pubkeys into m_bound via SHAKE256-384 under domain "lux-hybrid-sig-v1" — security ≥ max(EUF-CMA_secp256k1, sEUF-CMA_ML-DSA-65). Raw concat (the prior plan) only gives min security under non-honest-key adversary (CDFFJ23 §4). Classical = secp256k1 (matches existing P/X validator key format). PQ = ML-DSA-65 (FIPS 204). Use DeriveHybridIdentity for validator stake re-anchor flow: classical leaf at m/44'/9000'/serviceIndex'/0'/0', PQ leaf at m/44'/9000'/serviceIndex'/0'/1'. NodeID derived via single SHAKE256-384 over wire-form hybrid pubkey (no BTC-style double hash — cryptographer review confirmed single-SHAKE is sound). luxfi/kms v1.10.1 follows with the matching go.mod bump. Tests: go build ./... and go test -race -count=1 -short ./vms/platformvm/... PASS.
…DKG) Cascade: - magnetar v1.1.0 → v1.2.0 (closes MAGNETAR-PVSS-DKG-V11) - threshold v1.9.1 → v1.9.2 (magnetar bump) - consensus v1.25.8 → v1.25.9 (threshold + magnetar bump) Magnetar v1.2.0 lands a Schoenmakers-style PVSS-DKG over GF(257) for THBS-SE setup. No trusted dealer; no party ever holds the master byte vector at any time during setup. Share-envelope wire shapes are byte-shape-identical to the dealer path, so already- deployed share material is forward-compatible.
…old v1.9.2 → v1.9.4
PULSAR-V04-CTX cascade landing:
- pulsar v1.0.23 → v1.1.1 (v0.4 ctx-bound algebraic-aggregate
threshold sign — full Round1→Round2W→
Round2Sign→AlgebraicAggregateCtx with
FIPS 204 §5.4 ctx threaded into μ; no
single-party dealer shortcut anywhere)
- threshold v1.9.2 → v1.9.4 (dispatcher pulsar.Sign_Ctx rewired
onto full algebraic-aggregate path; no
master sk materialised in dispatcher
process at any point during sign)
- consensus v1.25.9 → v1.25.11 (passes the bumps through)
Test gates green on tip:
GOWORK=off go test -count=1 -short -timeout 600s ./vms/platformvm/...
GOWORK=off go test -count=1 -short -timeout 600s ./network/...
GOWORK=off go test -count=1 -short -timeout 600s ./consensus/...
…hold v1.9.7 + kms v1.11.2 + pulsar v1.1.2 Whole-tree sync to latest closure-swarm tags. accel v1.1.8 ships c_api.h via //go:embed so go mod vendor preserves it (fixes fresh-clone CI builds across all consumers).
GitHub-hosted macos-13 queues block the release pipeline for hours. The build is already pure Go cross-compile (CGO_ENABLED=0 GOOS=darwin GOARCH=$arch) so there's no reason to use a Mac runner. Route through the self-hosted lux-build ARC pool (fast, plentiful) instead. Replace 7z (not on ubuntu) with apt-installed zip in the same step. Output filename + artifact name are unchanged.
luxfi/evm v0.18.16 fixes the nil-chainConfig panic at PQ gate that made v1.28.15's image-baked EVM plugin crash on fresh-PVC pq:true bootstrap. Discovered during localnet 1337 bring-up (#148). Root cause in v0.18.15: vm.chainConfig.PQ was being set BEFORE vm.chainConfig was assigned from g.Config — nil-deref in plugin/evm/vm.go Initialize(). v0.18.16 moves the gate AFTER assignment. This unblocks v1.28.16 image build → unblocks localnet 100% green → unblocks cluster deploys (devnet → testnet → mainnet).
…ample We don't use gcr.io across lux/hanzo/zoo. Switch the heartbeat-tx example runtime stage from gcr.io/distroless/static-debian12:nonroot to FROM scratch, copying ca-certs, tzdata, and passwd/group from the builder.
…testnet+zoo-mainnet genesis embeds)
Embedded //go:embed configs/{mainnet,testnet,devnet,localnet} now contains
canonical 2-alloc genesis.json's reverted for RLP import compatibility:
- mainnet C-Chain (96369): block 0 = 0x3f4fa2a0... MATCH lux-mainnet-96369.rlp
- testnet C-Chain (96368): block 0 = 0x1c5fe377... MATCH lux-testnet-96368.rlp
- zoo-mainnet (200200): block 0 = 0x7c548af4... MATCH zoo-mainnet-200200.rlp
Each had been wedged by 43 PQ precompiles baked into config.precompileUpgrades
at blockTimestamp:0, mutating state root + producing non-canonical hash.
Activations moved to forward-dated upgrade.json (blockTimestamp 1766708400).
Also bumps pkg/genesis/security to v1.13.8 to stay version-locked.
Tidy drops bft v0.1.5 (no longer indirect).
…alidators produce blocks
On a strict-PQ chain a peer's consensus identity is its ML-DSA-65 NodeID
(StakingConfig.DeriveNodeID), but the network layer kept every peer on the
TLS-cert NodeID derived during the transport upgrade. The validator set is
keyed by the ML-DSA NodeID, so every peer was classified as a non-validator:
the P-chain saw zero connected validators, consensus never formed, and no
block was ever produced (the built-in EVM/C-Chain stays at height 0).
Two coupled defects:
1. network.NewNetwork built the PQ handshake identity with
peer.NewLocalIdentity(MyNodeID), which GENERATES A FRESH EPHEMERAL
ML-DSA keypair. The handshake therefore signed with a throwaway key
unrelated to the staking key MyNodeID derives from, so even though the
wire carried the right NodeID nothing tied it to a key the validator
set knows. (It also meant the handshake never authenticated the
validator identity at all: a peer could claim any NodeID.)
2. peer.runPQHandshakeIfRequired discarded HandshakeResult.PeerNodeID and
left p.id on the transport TLS-cert NodeID.
Fix:
- Thread the node's persistent staking ML-DSA keypair
(StakingConfig.StakingMLDSA{,Pub}) onto network.Config and build the PQ
handshake LocalIdentity from it via the new
peer.NewLocalIdentityFromStakingKey. The handshake now signs with the
same key that derives MyNodeID.
- After a successful handshake, peer.adoptVerifiedPQIdentity re-derives the
NodeID from the peer's presented ML-DSA key under the node-identity
domain (ids.Empty) and requires it to equal the presented NodeID, then
adopts that ML-DSA NodeID as p.id. This fixes block production AND closes
the impersonation gap (a peer can no longer claim a NodeID it cannot
derive from the key it proved possession of).
Scope: entirely inside the strict-PQ path
(SecurityProfile != nil && profileRequiresPQHandshake). Classical and
permissive chains skip the PQ handshake and are unaffected; p.id stays the
TLS-cert NodeID exactly as before. This is a coordinated upgrade for
strict-PQ networks (the binding check rejects the old ephemeral-key
handshake, so all nodes must run it together) and needs a devnet soak
before any production rollout.
Adds white-box tests for the bind / adopt / reject paths.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On a strict-PQ chain a peer's consensus identity is its ML-DSA-65 NodeID (
StakingConfig.DeriveNodeID), but the network layer kept every peer on the TLS-cert NodeID derived during the transport upgrade (peer/upgrader.go→ids.NodeIDFromCert). The validator set is keyed by the ML-DSA NodeID, so every peer was classified as a non-validator → the P-chain saw zero connected validators → consensus never formed → no block was ever produced (the built-in EVM/C-Chain stays at height 0, RPC serves reads buteth_blockNumbernever advances).This was observed on the Liquidity strict-PQ devnet: 3 validators, all healthy, all BLS-correct, peer mesh formed — but P-chain height stuck at 0.
Root cause — two coupled defects
1. The PQ handshake signed with an ephemeral key, not the staking key.
network.NewNetworkbuilt the handshake identity withpeer.NewLocalIdentity(MyNodeID), which generates a fresh ML-DSA keypair per process (see its doc-comment). So the handshake signature proved possession of a throwaway key with no relationship to the staking key thatMyNodeIDderives from. The wire carried the right NodeID, but nothing bound it to a key the validator set knows. (Corollary: the handshake never actually authenticated the validator identity — a peer could assert any NodeID.)2. The verified peer NodeID was discarded.
peer.runPQHandshakeIfRequiredusedHandshakeResult.AEADKeybut droppedHandshakeResult.PeerNodeID, leavingp.idon the transport TLS-cert NodeID. Consensus then looked up that TLS NodeID in an ML-DSA-keyed validator set and found nothing.Fix
StakingConfig.StakingMLDSA{,Pub}) ontonetwork.Config(mirrored innode.Node.initNetworking) and build the handshakeLocalIdentityfrom it via the newpeer.NewLocalIdentityFromStakingKey. The handshake now signs with the same key that derivesMyNodeID.peer.adoptVerifiedPQIdentity(new): after a successful handshake, re-derive the NodeID from the peer's presented ML-DSA key under the node-identity domain (ids.Empty— the exact domainDeriveNodeIDuses forMyNodeID) and require it to equal the presented NodeID, then adopt that ML-DSA NodeID asp.id.This both fixes block production and closes an identity-impersonation gap: a peer can no longer claim a NodeID it cannot derive from the key it proved possession of.
Because
peer.Startruns the handshake synchronously before the message-pump goroutines and beforenetwork.upgradeadds the peer toconnectingPeers/connectedPeers,p.idis already the ML-DSA NodeID by the time any peer-set bookkeeping keys by it — no re-keying race.Blast radius
Entirely inside the strict-PQ path (
SecurityProfile != nil && profileRequiresPQHandshake). Classical / permissive chains skip the PQ handshake and are unaffected —p.idstays the TLS-cert NodeID exactly as before. No wire-format change (same INIT/RESP frames); only which key signs, plus an added local verification.Rollout / review notes
getCurrentValidatorslists all NodeIDs with weight + EVMeth_blockNumberincrements) before any production rollout. Drafted for chain-team review; do not blind-merge/deploy to a live network.Tests
go build ./...clean;go vetclean;go test ./network/peer/...green. Addsnetwork/peer/pq_identity_adopt_test.gocovering: bound identity → adopted; unbound/forged NodeID → rejected + identity untouched; nil/empty result → rejected.Follow-ups (not in this PR)
MyNodeIDself-check andAllowConnectioninnetwork.upgradestill evaluate the TLS-cert NodeID (best-effort pre-filters; authoritative gating is post-handshake onp.id). Worth migrating to the ML-DSA NodeID for completeness.peersLockinnetwork.upgrade; a slow peer serializes connection establishment. Pre-existing; orthogonal to this fix.