Kills the bug CLASS behind the α-of-K finality wedge (v1.30.65), not just the
one Gossip instance. The chain router (node/chain_router.go) forwards an inbound
message only if message.ToConsensusOp maps it, then dispatches on the router op;
the node op table and the consensus router op enum are two halves of ONE routing
contract. When they diverged — router.Gossip existed but no node op mapped to it
— every broadcast vote was dropped as "unhandled message op" and finality
wedged, with nothing to catch it.
- ToConsensusOp now returns the router constants (byte(router.Get) ...) instead
of 0..11 magic numbers, so values cannot drift and the table reads as the
correspondence it is. Bumps consensus v1.25.29 -> v1.25.30 (adds router.NumOps,
the single source of truth for the op count).
- message/ops_test.go TestToConsensusOp_TableAlignedWithRouter is now EXHAUSTIVE
and BIDIRECTIONAL: it reconstructs the inverse mapping over the whole node op
space and requires a bijection onto [0, router.NumOps). A router op with no
node preimage (the original bug), a collision, a wrong target, or a stray
mapping all go RED. Pinned to router.NumOps, so a future op added to one table
but not the other fails the test instead of at runtime. Proven RED against the
original bug (drop the GossipOp case -> router.Gossip loses its preimage ->
fail); folds in the old Gossip-specific and hand-listed table checks.
Build rc=0 (GOWORK=off go build ./...); message tests green; finality fix
(GossipOp -> router.Gossip -> blockHandler.Gossip) unchanged and still covered.
The α-of-K quorum vote/cert transport rides on app-gossip (GossipOp), but
GossipOp was never added to the chain router's consensus-op table
(message.ToConsensusOp) nor to blockHandler.HandleInbound's switch. So every
inbound vote was dropped at node/chain_router.go as "unhandled message op"
before it could reach blockHandler.Gossip -> engine.HandleIncomingVote. Blocks
ride PutOp (mapped) and propagated+verified on all validators; votes ride
GossipOp (unmapped) and vanished. Each node held only its own self-vote, the
cert never reached alpha, and every chain wedged at height N: "verified but
never accepted", no vote/cert/accept activity.
This is the fork divergence from avalanchego, where votes are Chits — a
first-class consensus op always in the router table (snow/engine/snowman
engine.go Chits -> voter -> topological.RecordPoll -> accept). Lux moved votes
onto app-gossip but left the node-side op routing incomplete; the consensus
repo already reserved router.Gossip=11 'routed via the blockHandler Gossip
method' (core/router/router.go) — only the node wiring was missing.
Fix (node-only; safety core untouched — this is purely liveness):
- message.ToConsensusOp: map GossipOp -> 11 (router.Gossip)
- blockHandler.HandleInbound: add case handler.Gossip -> b.Gossip(...)
b.Gossip already demuxes the quorum envelope (Mode-gated) into
HandleIncomingVote / HandleIncomingCert; the signed-vote verify + verified-2/3
-stake cert assembly are unchanged, so honest votes now reach the assembler
without weakening VerifyWeighted or the unforgeable cert.
Regression guard (message/ops_test.go): GossipOp must map to router.Gossip, the
full op table must stay aligned with the consensus router, and an inbound vote
gossip must survive router extraction (op + envelope bytes) intact.
* feat(platformvm): CreateSovereignL1Tx — single-tx sovereign L1 launch
Adds a new platformvm tx type that atomically registers a sovereign L1
in one P-chain commit. Replaces what is today the four-step flow:
CreateNetworkTx + AddChainValidatorTx ×N + CreateChainTx ×K + ConvertNetworkToL1Tx
with one signed tx. After commit, the primary network has a permanent
record of the L1's network ID + initial validator set + chain manifest
+ on-chain validator-manager contract — but it does NOT track-chains
or validate the L1's blocks. The L1 runs its own consensus from
genesis. L2/L3/L4 follow the same pattern recursively.
Type shape:
type CreateSovereignL1Tx struct {
BaseTx
Owner fx.Owner // CreateNetworkTx parity
Validators []*ConvertNetworkToL1Validator // genesis validator set
Chains []*SovereignL1Chain // VM ID + genesis blob per chain
ManagerChainIdx uint32 // index into Chains[]
ManagerAddress types.JSONByteSlice // validator-manager contract
}
type SovereignL1Chain struct {
BlockchainName string
VMID ids.ID
FxIDs []ids.ID
GenesisData []byte
}
SyntacticVerify enforces:
- at least one validator (sorted, unique)
- at least one chain, ≤ MaxSovereignL1Chains (16)
- ManagerChainIdx is in range of Chains[]
- ManagerAddress ≤ MaxChainAddressLength
- per-chain name + VMID + FxIDs + genesis bounds
- BaseTx + Owner + each Validator each verify
Wired into:
- Visitor interface
- codec (registered as the next tx type after ConvertNetworkToL1Tx)
- signer + complexity + metrics + executor stubs across all visitor
implementations
Executor body is stubbed with a TODO. The atomic state transition
(mint new networkID from tx hash, seed validator-manager state,
register each Chain, charge fee) lands in a follow-up PR. This PR is
the type definition + interface plumbing so downstream tools (wallet,
CLI, fee calc, metrics) can target the tx type while the executor is
implemented.
* chore(node): kill subnet — chain/network vocabulary across node
Zero remaining `subnet|Subnet|SUBNET` in node Go source. Per canonical
no-subnet rule.
## Wire types (Go fields only; byte-level wire encoding unchanged)
message/wire/types.go TrackedSubnets → TrackedChains
message/wire/zap.go same on Read/Write
proto/p2p/p2p_zap.go SubnetUptime alias → ChainUptime
(consumes luxfi/proto rename in companion PR)
## Comment scrub
message/wire/types.go "(chain, subnet) pair" → "(chain, network) pair"
network/peer/handshake.go "primary-network or subnet" → "primary-network or per-chain"
node/node.go "per-subnet" → "per-chain"
"P→subnet warp" → "P→chain warp"
genesis/builder/builder.go "their own subnets" → "their own chains"
vms/platformvm/client.go dropped "subnet jargon" reference
vms/platformvm/service.go dropped "net / subnet jargon" reference
vms/platformvm/config/internal.go "subnet-spawned blockchain" → "per-chain blockchain"
## ICPSubnet (Internet Computer adapter)
ICPSubnet → ICPNet (type rename — unrelated to platform subnet,
but still a subnet word; killed for consistency)
map field `subnets` → `nets`
## Examples
wallet/network/primary/examples/bootstrap-hanzo/main.go:
--subnet-id → --network-id (CLI flag)
existingSubnetID local var → existingNetID
"subnet ID" log lines → "network ID"
"SUBNET_ID=" output → "NETWORK_ID="
"subnet-evm VM ID" → "EVM VM ID"
All comments rephrased.
wallet/network/primary/examples/heartbeat-tx/main.go: one comment
rephrase.
go.work workspace cleanup:
- Removed ./operator/go entry (placeholder; lux/operator polyglot
layout pending the cross-repo migration)
- Removed ./operator entry (mid-migration; nothing to build)
Build clean. Zero `grep -rIn "subnet|Subnet" --include="*.go"` matches.
The companion commit `7496606282` retired the gRPC fallback inside
vms/rpcchainvm/. This commit closes the loop by deleting every
remaining `//go:build grpc` file under node/, removing the dual-build
plumbing entirely. ZAP is the only wire protocol; there is no
`-tags=grpc` opt-in.
Per the "one and only one way to do everything" mandate, the
backwards-compat scaffolding (gRPC adapters, protoc stubs, connect-go
example handlers, OTLP gRPC exporter, x/sync gRPC sync engine,
keystore-over-gRPC client/server, rpcwarp gRPC signer, gRPC alias
reader) is removed forward-only — no aliases, no deprecation period.
Deletions (84 files):
- proto/pb/{aliasreader,http,io,keystore,message,messenger,net,p2p,
platformvm,rpcdb,sdk,sender,sharedmemory,signer,sync,
validatorstate,vm,warp}/ — protoc stubs (entire tree)
- proto/{p2p,platformvm,sync,vm}/*_grpc.go — gRPC type re-exports
- db/rpcdb/{grpc_server,grpc_client,grpc_test}.go — rpcdb gRPC adapter
- service/keystore/rpckeystore/ — keystore-over-gRPC (dead consumer)
- x/sync/ — entire merkledb sync engine (100% gRPC-tagged)
- internal/ids/rpcaliasreader/ — gRPC alias reader (dead consumer)
- connectproto/ — connect-go XSVM ping handler scaffolding
- vms/platformvm/warp/rpcwarp/{client,server}.go — gRPC warp signer
- vms/platformvm/network/warp.go — protobuf-based warp justification
handler (warp_zap.go retains the no-op verifier consumers expect)
- vms/components/message/message_grpc.go + message_test.go
- vms/example/xsvm/api/ping.go + vm_http_grpc.go + cmd/{run,xsvm}/
- wallet/network/primary/examples/sign-l1-validator-* (5 dead
example main packages)
- trace/{exporter_grpc,exporter_type,exporter_type_test,noop,tracer}.go
(OTLP gRPC exporter + duplicate Tracer types — trace_zap.go has
the canonical Tracer interface + no-op tracer)
- message/bft_grpc.go (Simplex BFT wrapper)
Edits (9 files): every surviving `_zap.go` drops its `//go:build !grpc`
constraint (the files are unconditional now) and drops stale
"ZAP version" / "ZAP mode" inline comments.
Doc updates:
- LLM.md ZAP Transport section: remove the `-tags=grpc` opt-in
language. Replace with "ZAP is the only wire protocol... there is
one and only one way". Update Latest Tag to v1.26.31. Update the
rpcdb topology section to reflect single-adapter state.
go.mod: connectrpc.com/connect, connectrpc.com/grpcreflect,
otlptracegrpc moved out of direct deps. google.golang.org/grpc +
protobuf demoted to indirect (still pulled transitively via luxfi/dex).
Verified:
- `go build ./...` (default, no tags) clean
- `go test ./db/rpcdb/... ./trace/... ./message/... ./vms/rpcchainvm/...
./vms/components/message/... ./vms/platformvm/network/...
./vms/example/xsvm/... ./service/keystore/... ./proto/...` clean
- `grep -rln '//go:build grpc' --include='*.go' node/` returns zero
- `grep -rln '//go:build !grpc' --include='*.go' node/` returns zero
- `grep -rln 'google.golang.org/grpc' --include='*.go' node/` returns
zero (only transitive deps remain in go.sum)
Pre-existing TestGraniteNetworkIDConfiguration failure in tests/ is
unrelated — fails on the parent commit too (verified via stash).
Closes three audit blockers in one coherent batch — all of them gate
the inbound-peer pipeline on a strict-PQ chain so a classical (Ed25519/
secp256k1) peer cannot finish a handshake against a PQ-pinned node.
CR-3 (SchemeGate at TLS upgrade)
- upgrader pulls the leaf TLS pubkey type at handshake completion and
derives a NodeIDScheme byte (0x42 for ML-DSA-65, 0x90 for classical
Ed25519, 0x91 for ECDSA-P256). The chain's
ChainSecurityProfile.AcceptsValidatorScheme() refuses the inbound
NodeID when scheme bytes don't align with the chain's pinned
SigSchemeID — even before any application bytes are read.
- Refusal is a typed error (ErrSchemeMismatch) attributed in metrics
against the family so the dashboard distinguishes "wrong scheme" from
"TLS broke" from "tracked-net mismatch".
CR-5 (PQ-only handshake handoff)
- peer.Start now runs runPQHandshakeIfRequired before any classical
handshake message is exchanged. Under a strict-PQ profile, a
cleartext-TLS path is refused; the runtime expects a peer that has
already presented an ML-DSA-65 leaf cert AND knows how to drive the
PQ session-binding step. Test coverage in upgrader_strict_pq_test.go.
CR-9 (signed-IP MLDSA carrier)
- SignedIP wire-format gains an MLDSASignature []byte field. Encoded
append-only on the gossip wire (Reader.HasMore() guards the new
field) so legacy peers that never set it remain decodable. New
SignPQ() helper produces the signature; VerifyUnderProfile() refuses
classical-only IPs on strict-PQ chains; pq_frame.go gives
fuzz-friendly canonical encoding.
- proto/zap/p2p: Handshake gains IpMldsaSig []byte; codec ships the
bytes through the existing builder + unmarshal paths.
- message.OutboundMsgBuilder.Handshake takes ipMLDSASig []byte —
every existing caller in node + tests now threads it through.
Wiring
- n.Config.NetworkConfig.SecurityProfile is set from
n.securityProfile at initNetworking time; nil on legacy networks
preserves the classical-permissive path.
- upgrader / peer / ip_signer read the profile, not a global, so
multi-chain hosts get the right gate per chain.
Tests: network/peer/{ip_pq_test.go, ip_pq_wire_test.go,
upgrader_strict_pq_test.go} pin the gates; go test
./network/peer/ -count=1 -short passes (6.5s).
Module bumps:
- github.com/luxfi/geth v1.16.91 (MLDSATxType + per-chain PQ gate)
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.