This file provides guidance for AI assistants working with the Lux node codebase.
Lux blockchain node implementation - a high-performance, multi-chain blockchain platform written in Go. Features multiple consensus engines (Chain, DAG, PQ), EVM compatibility, and a multi-chain architecture with specialized capabilities.
Key Context:
- Original Lux Network node — NOT a fork
- Latest Tag: v1.26.31
- Network ID: 96369 (Lux Mainnet), 96368 (Testnet), 96370 (Devnet)
- Go Version: 1.26.1+
- Database: ZapDB (primary, default)
Node now consumes the locked ChainSecurityProfile end-to-end and enforces
strict-PQ at four boundaries: peer handshake, mempool, validator scheme
selection, and EVM contract auth.
node/node.go:initSecurityProfile(F102 closure) loads the chain-wide profile from genesis at boot, hashes it, and pins it into every chain's bootstrap. Resolved profile is what every downstream verifier consults.network/peer/scheme_gate.go—SchemeGate.Classify(presented, site)funnels every inbound NodeID through the profile'sAcceptsValidatorScheme. Wire-typed NodeID (luxfi/idsTypedNodeID + scheme byte) is the canonical handshake form.vms/txs/auth/policy.go—ClassicalCompatRegistry+ strict-PQ mempool gate. Bothplatformvm(P-Chain) andavm(X-Chain) mempools refuse classical credentials when the resolved profile hasForbidECDSAContractAuth=true.vms/mldsafx/— re-exports the consensusmldsafxUTXO feature extension as the node-owned UTXO surface (ML-DSA-65 verify).network/peerPQ handshake — ML-KEM-768 / ML-KEM-1024 KEM + ML-DSA-65 identity (dc906d281b).
| SHA | Tag | Impact |
|---|---|---|
| (pending) | next | LP-023 Phase 1 batch 2: 5 more native-ZAP tx types — BaseTx v1, RegisterL1ValidatorTx v1, SlashValidatorTx v1, TransferChainOwnershipTx v1, RemoveChainValidatorTx. Cross-type Parse mean speedup 8.5× over linearcodec. Variable-length nested-object schemas (Outs/Ins/full OutputOwners/Warp Message/Evidence) deferred to batch 3. |
e77a7ef78e |
(pre) | LP-023 Phase 1 batch 1: 4 simple tx types + bench harness. 37× Parse, 5.6× cross-type mean. |
9df72a6f55 |
v1.26.10 | Wire ChainSecurityProfile into bootstrap (closes F102) |
c4af52411e |
v1.26.10 | X-Chain (avm) mempool refuses classical creds under strict-PQ |
a14a1601f4 |
v1.26.10 | P-Chain (platformvm) mempool refuses classical creds under strict-PQ |
1cf0aa80ca |
v1.26.10 | ClassicalCompatRegistry + strict-PQ mempool gate |
a0f4f4b21c |
v1.26.10 | vms/mldsafx: re-export ML-DSA feature extension |
448fdeb7a1 |
v1.26.10 | ML-DSA-65 promoted to canonical NodeID under strict-PQ |
dc906d281b |
v1.26.10 | PQ peer handshake — ML-KEM-768/1024 + ML-DSA-65 identity |
- Repo:
v1.26.12(next bump:v1.26.13). - Pinned:
consensus v1.23.4+(neededValidatorSchemeID),crypto v1.18.5,ids v1.2.9(will move to v1.2.10 in next bump forTypedNodeID),genesis v1.9.6.
luxfi/consensus→ profile + auth + zchain typesluxfi/crypto→ ML-DSA / ML-KEM / SLH-DSA primitivesluxfi/genesis→ genesis-pinned profile (Resolveat load)luxfi/ids→TypedNodeIDwire form (consumed at handshake)luxfi/geth→ EVM (forvm.SetActiveSecurityProfileinstall point)
- Profile resolve at boot:
node/node.go:initSecurityProfile - Profile RPC + REST + metrics:
service/security/- JSON-RPC namespace:
securityatPOST /v1/security(methodssecurityProfile,blockSecurity) - REST sidecars:
GET /v1/security/profile,GET /v1/security/block/{n} - Prometheus gauges:
/v1/metricsunder thesecurity_*family
- JSON-RPC namespace:
- Peer scheme gate:
network/peer/scheme_gate.go - Classical-compat registry:
vms/txs/auth/policy.go - Mempool gate (P-Chain):
vms/platformvm/mempool/*.go - Mempool gate (X-Chain):
vms/avm/mempool/*.go - ML-DSA feature extension:
vms/mldsafx/
vms/zkvm/accel/still soft-falls-back when CGO is disabled; Z-Chain proof verification path needs CGO-required mode for production strict-PQ.vm.SetActiveSecurityProfileinstall point exists inluxfi/geth/core/vmbut EVM-side contract-auth refusal still needs a chain-bootstrap call (F102 wiring closes the consensus side; geth-side hookup is the remaining tail).
Every Lux VM that accepts user-submitted txs declares a fee.Policy
(package vms/types/fee). There is one interface and one validator —
no per-VM bespoke fee structs.
| VM | Chain | Posture | Policy |
|---|---|---|---|
| dexvm | D-Chain | user-tx | FlatPolicy{Fee: MinTxFeeFloor, AssetID: UTXOAssetIDFor(networkID)} |
| zkvm | Z-Chain | user-tx | FlatPolicy{Fee: MinTxFeeFloor, ...} |
| aivm | A-Chain | user-tx | FlatPolicy{Fee: MinTxFeeFloor, ...} |
| keyvm | K-Chain | user-tx | FlatPolicy{Fee: MinTxFeeFloor, ...} |
| bridgevm | B-Chain | user-tx | FlatPolicy{Fee: MinTxFeeFloor, ...} |
| quantumvm | Q-Chain | user-tx | FlatPolicy{Fee: MinTxFeeFloor, ...} |
| identityvm | I-Chain | user-tx | FlatPolicy{Fee: MinTxFeeFloor, ...} |
| thresholdvm | M-Chain | service-only | NoUserTxPolicy{} |
| oraclevm | O-Chain | service-only | NoUserTxPolicy{} |
| relayvm | R-Chain | service-only | NoUserTxPolicy{} |
| graphvm | G-Chain | read-only | NoUserTxPolicy{} (GraphQL refuses mutation) |
| evm | C-Chain | user-tx | native EVM gas (gas * gasPrice >= 0 enforced upstream) |
| platformvm | P-Chain | user-tx | native TxFee field on Config |
| avm | X-Chain | user-tx | native TxFee field on Config |
MinTxFeeFloor = 1 mLUX = 1_000_000 nLUX (the same minimum the P-Chain
base fee enforces). User-facing chains MAY charge more; they MUST NOT
charge less.
- VM struct holds
feePolicy fee.Policy(andnetworkID uint32). InitializesetsfeePolicy = fee.FlatPolicy{...}(orfee.NoUserTxPolicy{}for service-only) frominit.Runtime.NetworkIDand callsfee.Validate(vm.feePolicy)— refuses zero-fee user-facing chains at boot, before any block is accepted.- The canonical user-tx admission entry (e.g.
SubmitTx,IssueTx,InitiateBridgeTransfer, mutating service RPCs) callspolicy.ValidateFee(paid, asset)BEFORE mempool insert. - Consensus-internal paths (engine→VM block delivery, replay, internal tx emission) bypass the fee gate — the policy gates only the user-submitted entrypoint.
vms/types/fee/policy.go— interface + FlatPolicy + NoUserTxPolicy + Validate~/work/lux/chains/<vm>/feegate.go— per-VM helper + gate method~/work/lux/chains/<vm>/feegate_test.go— RejectsZeroFee + AcceptsMinFee- Oracle (O-Chain):
~/work/lux/oracle/vm/feegate.go(re-exported by~/work/lux/chains/oraclevm/) - Relay (R-Chain):
~/work/lux/relay/vm/feegate.go(re-exported by~/work/lux/chains/relayvm/) - Graph (G-Chain):
~/work/lux/chains/graphvm/feegate.go(read-only; NoUserTxPolicy)
The ONE way to build + publish releases is RELEASE.md:
platform.hanzo.ai reads hanzo.yml on a v* tag push and
schedules the image build onto self-hosted arcd pools (lux-build-linux-*)
over the native long-poll fabric — no GitHub-Actions hop. ONE Dockerfile
build yields BOTH artifacts: the node image (ghcr.io/luxfi/node:vX.Y.Z, luxd
- 12 baked VM plugins) and, via
scripts/publish_plugin_set.sh, the plugin set tos3://lux-plugins-<env>/<pluginset>/(operatorpluginSource). The.github/workflows/*build/release workflows are retired (RELEASE.md §Retire).
# Build node binary
./scripts/run_task.sh build
# Output: ./build/luxd
# Build specific components
go build -o luxd ./app# Run all tests
go test ./... -count=1
# Run specific package
go test ./vms/platformvm/state -count=1
# With race detection
go test -race ./...# Generate mocks
go generate ./...
# Regenerate protobuf
./scripts/run_task.sh generate-protobuf# Mainnet
./build/luxd
# Testnet
./build/luxd --network-id=testnet
# Local network
lux network startPrimary network (P/X/C) uses Quasar consensus via luxfi/consensus.
All new native chains use Quasar (BLS + Corona + ML-DSA).
| Chain | Purpose | VM | Consensus |
|---|---|---|---|
| P-Chain | Staking, validators, L1 validators | PlatformVM | Quasar |
| X-Chain | UTXO-based asset exchange | XVM | Quasar |
| C-Chain | EVM smart contracts | EVM | Quasar |
| A-Chain | AI inference, model registry | AIVM | Quasar |
| B-Chain | Cross-chain bridge operations | BridgeVM | Quasar |
| D-Chain | DEX (order book, perpetuals) | DexVM | Quasar |
| G-Chain | On-chain graph database | GraphVM | Quasar |
| I-Chain | Decentralized identity (DID/VC) | IdentityVM | Quasar |
| K-Chain | Post-quantum key management | KeyVM | Quasar |
| M-Chain | Threshold signing (MPC) | ThresholdVM | Quasar |
| O-Chain | Oracle price feeds | OracleVM | Quasar |
| Q-Chain | Post-quantum consensus coordination | QuantumVM | Quasar |
| R-Chain | Cross-chain message relay | RelayVM | Quasar |
| S-Chain | Service node coordination | ServiceNodeVM | Quasar |
| T-Chain | Cross-chain teleport (bridge+relay+oracle) | TeleportVM | Quasar |
| Z-Chain | Zero-knowledge proofs (FHE) | ZKVM | Quasar |
Located in /consensus/ (separate package github.com/luxfi/consensus):
- Quasar: Production consensus -- BLS12-381 + Corona (lattice) + ML-DSA-65 (FIPS 204)
- Chain Engine: Linear blockchain consensus (Nova sub-protocol)
- DAG Engine: Directed acyclic graph for parallel processing (Nebula sub-protocol)
- PQ Engine: Post-quantum finality layer
Sub-protocols: Photon (sampling) -> Wave (voting) -> Focus (confidence) -> Ray/Field (finality)
Located in /vms/:
- platformvm: Staking, validation, network management
- xvm: Asset transfers, UTXO model
- dexvm: DEX with order book, perpetuals, AMM
- thresholdvm: Threshold MPC and FHE for confidential computing
- quantumvm: PQ consensus coordination (ML-DSA, Corona)
- identityvm: Decentralized identity (DID, verifiable credentials)
- keyvm: Post-quantum key management (ML-KEM, ML-DSA)
- bridgevm: Cross-chain bridge with MPC attestation
- oraclevm: Decentralized oracle network
- aivm: AI inference verification
- graphvm: On-chain graph database
- relayvm: Cross-chain message relay
- servicenodevm: Service node epoch management
- teleportvm: Unified bridge+relay+oracle
- zkvm: Zero-knowledge proof verification
- proposervm: Block proposer wrapper VM
p2p.Sender (from github.com/luxfi/p2p):
type Sender interface {
SendRequest(ctx context.Context, nodeIDs set.Set[ids.NodeID], requestID uint32, request []byte) error
SendResponse(ctx context.Context, nodeID ids.NodeID, requestID uint32, response []byte) error
SendError(ctx context.Context, nodeID ids.NodeID, requestID uint32, errorCode int32, errorMessage string) error
SendGossip(ctx context.Context, config SendConfig, msg []byte) error
}Keychain Interfaces (from github.com/luxfi/keychain):
type Signer interface {
SignHash([]byte) ([]byte, error)
Sign([]byte) ([]byte, error)
Address() ids.ShortID
}
type Keychain interface {
Get(addr ids.ShortID) (Signer, bool)
Addresses() set.Set[ids.ShortID]
}- ✅
github.com/luxfi/node - ✅
github.com/luxfi/geth(NOT go-ethereum) - ✅
github.com/luxfi/consensus - ✅
github.com/luxfi/keychain - ✅
github.com/luxfi/ledger - ✅
github.com/luxfi/lattice(FHE) - ❌
github.com/luxfi/*legacy upstream forks - ❌
github.com/ethereum/go-ethereum
Avoid conflicts with consensus packages:
import (
platformblock "github.com/luxfi/node/vms/platformvm/block"
consensusblock "github.com/luxfi/consensus/engine/chain"
)LUX uses 6 decimals (microLUX base unit) on P-Chain/X-Chain:
| Unit | Value |
|---|---|
| µLUX (MicroLux) | 1 (base) |
| mLUX (MilliLux) | 1,000 |
| LUX | 1,000,000 |
| TLUX (TeraLux) | 10^18 |
Supply Cap: 2 trillion LUX (2 × 10^18 µLUX)
C-Chain uses standard EVM 18 decimals (Wei).
See utils/units/lux.go for constants.
github.com/luxfi/genesis (JSON config) → github.com/luxfi/node/genesis/builder (type conversion)
- Genesis package has no node dependencies
- Builder package handles type conversions (string → ids.NodeID, uint64 → time.Duration)
These require CGO for full functionality (graceful fallback when disabled):
consensus/quasar- GPU NTT accelerationvms/thresholdvm/fhe- GPU FHE operationsx/blockdb- zstd compression
Located in vms/thresholdvm/fhe/:
- Uses
github.com/luxfi/lattice/multipartyfor DKG - Lattice-based cryptography only (no fallbacks)
- Threshold decryption via Warp messaging
Precompile Addresses:
| Precompile | Address |
|---|---|
| Fheos | 0x0200000000000000000000000000000000000080 |
| ACL | 0x0200000000000000000000000000000000000081 |
| InputVerifier | 0x0200000000000000000000000000000000000082 |
| Gateway | 0x0200000000000000000000000000000000000083 |
ZAP is the only wire protocol for VM<->Node communication. The gRPC
fallback (and its -tags=grpc opt-in) was retired in v1.26.31 along
with every //go:build grpc file under node/. There is one and only
one way to talk to a Chain VM: ZAP.
Build:
go build # ZAP only — there are no build tagsKey Packages:
github.com/luxfi/api/zap— Core wire protocol and message types (Layer A)github.com/luxfi/protocol/rpcdb— rpcdb service spec / data carriers (Layer B)github.com/luxfi/node/db/rpcdb— rpcdb Service + ZAP transport adapter (Layer C)vms/rpcchainvm/sender/— Node-sidep2p.Senderover ZAPvms/rpcchainvm/zap/— ChainVM client/server over ZAPvms/platformvm/warp/zwarp/— Warp signing over ZAP
rpcdb Layered Topology:
- Layer A — wire framing:
github.com/luxfi/api/zap - Layer B — rpcdb service spec:
github.com/luxfi/protocol/rpcdb - Layer C — rpcdb impl:
node/db/rpcdb/{service.go, zap_server.go}service.go— transport-neutralServicewrappingdatabase.Databasezap_server.go— ZAP transport adapter (only adapter)
- One Service, one transport. The dual-adapter pattern stays available
for future transports (each is a new file wrapping
*Service), but ZAP is the only one shipping.
Wire Protocol Format:
[4 bytes: length][1 byte: message type][payload...]
Performance Benefits:
- Zero-copy serialization (buffer pooling via sync.Pool)
- ~5-10x faster serialization than protobuf
- ~2-3x lower latency (no HTTP/2 overhead)
- ~30-50% CPU reduction on hot paths
Sender Usage:
// ZAP transport — the only transport
s := sender.ZAP(zapConn)Warp over ZAP:
The zwarp package implements warp signing via ZAP:
// Client implements warp.Signer over ZAP
client := zwarp.NewClient(zapConn)
sig, err := client.Sign(unsignedMsg)
// BatchSign for HFT optimization
sigs, errs := client.BatchSign(messages)The node supports RNS as an alternative transport layer alongside TCP/IP, enabling mesh networking, LoRa connectivity, and offline-first validator operation.
Specification: LP-9701
The net/endpoints package supports three addressing modes:
// IP address
endpoint := endpoints.NewIPEndpoint(netip.MustParseAddrPort("203.0.113.50:9631"))
// Hostname (DNS resolved)
endpoint, _ := endpoints.NewHostnameEndpoint("validator.example.com", 9631)
// RNS destination (mesh/LoRa)
endpoint, _ := endpoints.NewRNSEndpointFromHex("rns://a5f72c3d4e5f60718293a4b5c6d7e8f9")| File | Purpose |
|---|---|
net/endpoints/endpoint.go |
Unified endpoint abstraction (IP, hostname, RNS) |
network/dialer/rns_transport.go |
RNS transport implementation |
network/dialer/rns_identity.go |
Classical identity (Ed25519 + X25519) |
network/dialer/rns_identity_pq.go |
Hybrid PQ identity (+ ML-DSA + ML-KEM) |
network/dialer/rns_link.go |
Encrypted link protocol with PQ support |
network/dialer/rns_announce.go |
Destination discovery and announcements |
# ~/.lux/config.yaml
rns:
enabled: true
configPath: ~/.lux/reticulum
announceInterval: 5m
interfaces:
- AutoInterface
- TCPClientInterface
linkTimeout: 30s
postQuantum: true # Enable hybrid PQ mode
requirePostQuantum: false # Allow classical-only peersRNS transport supports hybrid post-quantum cryptography combining classical algorithms with NIST-standardized post-quantum primitives (TLS 1.3-like approach).
| Purpose | Classical | Post-Quantum | Security |
|---|---|---|---|
| Identity Signing | Ed25519 | ML-DSA-65 | NIST Level 3 |
| Key Exchange | X25519 | ML-KEM-768 | NIST Level 3 |
| Session Encryption | AES-256-GCM | - | 256-bit |
| Key Derivation | HKDF-SHA256 | - | - |
- Ephemeral Keys: Fresh X25519 + ML-KEM keypairs generated per session
- Key Destruction: Ephemeral private keys zeroed after handshake
- Hybrid Derivation:
combined_secret = X25519_shared || ML_KEM_shared - Defense-in-Depth: Secure if either algorithm remains unbroken
| Component | Classical | Hybrid | Delta |
|---|---|---|---|
| Public Identity | 64 bytes | ~3.2 KB | +3.1 KB |
| Signature | 64 bytes | ~2.5 KB | +2.4 KB |
| Key Exchange | 64 bytes | ~1.2 KB | +1.1 KB |
| Handshake Total | ~256 bytes | ~7.5 KB | +7.2 KB |
- Capability Exchange: Handshake advertises PQ support
- Graceful Fallback: Falls back to classical if peer lacks PQ
- Mixed Networks: PQ and classical validators coexist
- Policy Enforcement:
requirePostQuantum: truerejects classical peers
network/peer/scheme_gate.go (v1.26.10) is the single primitive that
turns a wire NodeID into a (scheme, NodeID) pair and runs the
cross-axis check against the chain's ChainSecurityProfile.
SchemeGate{Profile, ClassicalCompatUnsafe, ActivationHeight}is the chain-scoped policy object. One gate per chain, pinned at bootstrap.Classify(nodeID, scheme, height, site) (TypedNodeID, error)is the single entry point. Callers pass a site tag ("handshake","proposer","validator","mempool-sender") that appears in the refused-by error.- Migration path:
ActivationHeightis the block at which a strict-PQ chain refuses any non-PQNodeIDSchemebyte at every height under the forward-only PQ policy. The classicalsecp256k1(0x90) scheme is refused at the gate; there is no transition window and no operator classical-compat escape hatch (strict-PQ chains refuse classical at every boundary, period). - Typed errors:
ErrSchemeGateConfig,ErrSchemeGateMismatch,ErrSchemeGateUnknownScheme.
Wire form: TypedNodeID = (NodeIDScheme byte, 20-byte NodeID). The
20-byte storage/map-key form stays byte-identical; the scheme byte
travels on the wire so a receiver knows which verifier to dispatch
without trusting the chain profile alone.
# Run hybrid PQ tests
go test -v -run "TestHybrid" ./node/network/dialer/... -count=1
# Key tests:
# - TestHybridIdentity_SignVerify (ML-DSA-65 signatures)
# - TestHybridIdentity_Encapsulate_Decapsulate (ML-KEM-768)
# - TestHybridRNSLink_Handshake (full hybrid handshake)
# - TestHybridRNSLink_ForwardSecrecy (ephemeral key destruction)
# - TestHybridToClassical_Fallback (backward compatibility)Node's rpcchainvm implements p2p.Sender (from github.com/luxfi/p2p) for cross-chain messaging.
The sender package is the ZAP-native implementation of p2p.Sender.
Nodes don't automatically track chains. Use:
--track-chains=<ChainID>Or create config: ~/.lux/runs/.../node*/chainConfigs/<ChainID>.json
Mainnet genesis requires Cancun fork config:
"blobSchedule": {
"cancun": {
"max": 6,
"target": 3,
"baseFeeUpdateFraction": 3338477
}
}CLI creates new directories on restart. Use snapshots:
lux network save --snapshot-name <name>
lux network start --snapshot-name <name>For importing pre-merge blocks, Shanghai must be active based on ShanghaiTime, not merge status.
Problem: "db contains invalid genesis hash" error when restarting nodes.
Cause: Genesis bytes are rebuilt from JSON config on each start. Due to non-deterministic JSON serialization (map iteration order), the rebuilt bytes differ from the original, causing hash mismatch.
Solution: Genesis bytes are now cached to genesis.bytes file in the node's data directory. On subsequent restarts, the cached bytes are used directly. This happens automatically when using --genesis-file.
Problem: "failed to parse config: unknown codec version" for T-Chain (ThresholdVM) or Z-Chain (ZKVM) in dev mode.
Cause: Two issues:
- Genesis builder passes JSON config (
{"version":1,"message":"..."}) to VMs that expect binary codec format - Dev mode's automining config injection converts all chain configs to JSON, breaking binary-codec VMs
Solution:
genesis/builder/builder.go: T-Chain and Z-Chain use[]byte(config.TChainGenesis)(empty bytes for defaults) instead ofgetGenesis()which returns JSONchains/manager.go:injectAutominingConfigonly injects forEVMID, skipping binary-codec VMs
Alternative: Use --genesis-raw-bytes flag to pass base64-encoded pre-built genesis bytes directly.
The github.com/luxfi/node/vms/components/lux package contains a parallel
lux.UTXO/lux.TransferableInput type tree alongside github.com/luxfi/utxo.
External consumers (e.g. a white-label tenant's network-bootstrap tooling) need
to import the vms/components/lux variant to interop with PlatformVM/AVM
tx builders — luxfi/utxo types alone are not accepted by the X→P export
path. This is a known anomaly pending #58 follow-up consolidation; do NOT
collapse the two packages without that migration.
| Item | Path |
|---|---|
| luxd binary | ~/.lux/bin/luxd/luxdv*/luxd |
| VM plugins | ~/.lux/plugins/<VMID> |
| Network runs | ~/.lux/runs/local_network/network_* |
| Snapshots | ~/.lux/snapshots/ |
| Chain configs | ~/.lux/chain-configs/<BlockchainID>/ |
- Build node:
cd ~/work/lux/node && go build -o /tmp/luxd ./main - Install:
cp /tmp/luxd ~/.lux/bin/luxd/luxdv1.21.0/luxd - Build EVM:
cd ~/work/lux/evm && go build -o ~/.lux/plugins/<VMID> ./plugin - Start:
lux network start --mainnet
| Repo | Purpose |
|---|---|
~/work/lux/consensus |
Consensus engines (Chain, DAG, PQ) |
~/work/lux/geth |
C-Chain EVM implementation |
~/work/lux/evm |
EVM plugin |
~/work/lux/genesis |
Genesis configurations |
~/work/lux/cli |
Management CLI |
~/work/lux/netrunner |
Network testing |
~/work/lux/dex |
DEX implementation |
~/work/lux/standard |
Solidity contracts (including FHE) |
~/work/lux/lattice |
Lattice cryptography |
- Memory exhaustion protection (IP tracker limits, bloom filter caps)
- BLS signature CGO/pure-Go consistency
- Replay attack prevention with timestamp validation
- Safe math in DEX operations
Problem: New validator node stays at P-chain height 0 even after connecting to testnet peers. Blocks received via Put/PushQuery are silently discarded.
Root Cause: HandleIncomingBlock returns "not found" when the block's parent isn't in the local state. isMissingContextError didn't recognize "not found" as a missing-context condition, so requestContext (GetAncestors) was never called.
Fix in chains/manager.go, isMissingContextError:
// Added "not found" pattern:
strings.Contains(errStr, "not found") // parent block not in local stateEffect: Now when a block arrives whose parent is unknown, the handler sends GetAncestors to the peer, receives the full ancestor chain, and processes blocks in order, advancing the P-chain height.
Note: The network layer (network.go:sequencerID) already correctly maps native chain IDs (P, C, X, etc.) to PrimaryNetworkID for validator set lookups — no separate gossip fix needed.
When CGO disabled, these use CPU fallbacks:
consensus/quasar/gpu_ntt_nocgo.govms/thresholdvm/fhe/gpu_fhe_nocgo.govms/zkvm/accel/accel_mlx.go
Problem: C-chain and D-chain RPC endpoints returning 404 despite VMs running.
Cause: The zap.Client in vms/rpcchainvm/zap/client.go did not implement the CreateHandlers interface. The node checks for this interface to register HTTP handlers (like /rpc, /ws) with the HTTP server.
Solution: Added CreateHandlers method to zap.Client that:
- Sends
MsgCreateHandlersvia ZAP wire protocol to the VM - Receives
CreateHandlersResponsewith list of handlers (prefix + server address) - Creates
httputil.NewSingleHostReverseProxyfor each handler - Returns
map[string]http.Handlerfor registration
File Modified: vms/rpcchainvm/zap/client.go
Verification:
curl -s -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \
http://localhost:9640/v1/bc/C/rpc
# Returns: {"jsonrpc":"2.0","id":1,"result":"0x17870"}Feature: The node's root endpoint ("/") provides EVM compatibility and node information.
Behavior:
- GET /: Returns JSON node information (nodeId, networkId, version, chains, endpoints)
- POST /: Proxies JSON-RPC requests directly to C-chain
/v1/bc/C/rpc - OPTIONS /: Returns CORS preflight headers
Files Modified: server/http/router.go, server/http/server.go
Types:
type RootInfo struct {
NodeID string `json:"nodeId,omitempty"`
NetworkID uint32 `json:"networkId,omitempty"`
Version string `json:"version,omitempty"`
Ready bool `json:"ready"`
Chains struct { C, P, X string } `json:"chains"`
Endpoints struct { RPC, Websocket, Info, Health string } `json:"endpoints"`
}
type RootInfoProvider interface {
GetRootInfo() RootInfo
}Usage:
# Get node info
curl http://localhost:9650/
# Send EVM JSON-RPC directly to root (proxied to C-chain)
curl -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \
http://localhost:9650/Implementation Notes:
- The Server interface exposes
SetRootInfoProvider(provider)to configure node info - When no provider is set, returns default endpoint paths
- POST errors return proper JSON-RPC error format if C-chain unavailable
Problem: Health check shows "validator doesn't have a BLS key" despite BLS keys being correctly configured in genesis.
Cause: The initValidatorSets() function in /vms/platformvm/state/state.go was skipping validator population when NumNets() != 0. This happened because:
- Network layer might pre-populate validators (without BLS keys) before state initialization
- When
initValidatorSets()runs, it sees validators exist and skips adding them with proper BLS keys - The health check queries
n.vdrs.GetValidator()which returns validator with nil PublicKey
Solution: Modified initValidatorSets() to always add validators (not skip when NumNets() != 0). The AddStaker method replaces existing entries, so validators get updated with proper BLS keys.
File Modified: vms/platformvm/state/state.go (line ~2144)
Before:
if s.validators.NumNets() != 0 {
// skip re-adding them here
return nil
}After:
if s.validators.NumNets() != 0 {
log.Info("initValidatorSets: validator manager not empty, will update with BLS keys")
}
// Continue to add validators with proper BLS keysVerification:
curl -s http://localhost:9650/v1/health | jq '.checks.bls'
# Should show: "message": "node has the correct BLS key"Testing conducted on a single Lux validator node (testnet mode, macOS):
| Metric | Result |
|---|---|
| Sustained TPS | 1,091 TPS (60s benchmark) |
| Peak TPS | 1,094 TPS (5 workers) |
| Query Performance | 840 queries/sec |
| Query Latency | 17.67ms avg |
| Optimal Concurrency | 5 workers |
| Total Transactions | 65,497 txs/min |
Concurrency Scaling:
| Workers | TPS |
|---|---|
| 1 | 438 |
| 5 | 1,094 (optimal) |
| 10 | 684 |
| 20 | 521 |
Key Findings:
- Single node achieves ~1,100 TPS sustained with optimal concurrency
- Higher concurrency (>5 workers) decreases TPS due to nonce contention
- Query latency is consistent at ~18ms
- Testnet mode uses K=20 Lux consensus (vs K=1 dev mode)
Benchmark Command:
cd ~/work/lux/benchmarks
NODE_ENDPOINT="http://localhost:9640/v1/bc/C/rpc" \
PRIVATE_KEY="<funded_key>" \
./bin/bench tps --chains=lux --duration=60s --concurrency=5Encoding boundaries are one-way and explicit:
- External (HTTP / JSON-RPC API) —
github.com/go-json-experiment/json(v2). Neverencoding/json. This covers:service/*,server/*,pubsub/,vms/platformvm/service.go,vms/xvm/service.go, JSON-RPC clients (vms/platformvm/client_*), CLI tools (cmd/*), wallet examples, on-disk config files (read once at boot), genesis/upgrade blobs. - Internal (state, P2P, consensus, MPC, logs, metrics) — ZAP wire only.
No JSON in:
network/,consensus/,snow/,chains/(data-plane),vms/*/state/,vms/*/block/,vms/*/txs/(struct codec), threshold payloads, P2P message bodies, internal databases.
Migration helpers (v2 API delta vs v1):
| v1 (encoding/json) | v2 (go-json-experiment/json) |
|---|---|
json.Marshal(v) |
json.Marshal(v) (variadic opts; signature compat) |
json.MarshalIndent(v, "", " ") |
json.Marshal(v, jsontext.WithIndent(" ")) |
json.Unmarshal(b, &v) |
json.Unmarshal(b, &v) |
json.NewEncoder(w).Encode(v) |
json.MarshalWrite(w, v) (no trailing \n) |
json.NewDecoder(r).Decode(&v) |
json.UnmarshalRead(r, &v) |
json.RawMessage |
jsontext.Value |
*json.SyntaxError |
*jsontext.SyntacticError |
v2 semantic differences worth knowing (these change wire shape):
[N]bytefield with noMarshalJSON⇒ v2 marshals as base64 string, v1 marshalled as JSON array of byte numbers. AddMarshalJSONon the type if the array form is wanted on the wire.time.Duration⇒ v2 default is the standard string form ("30m"); v1 marshalled as int nanoseconds. v1 sub-package (github.com/go-json-experiment/json/v1) exposesFormatDurationAsNano(true); v2 root does not. Prefer the string form on new APIs.- v2 enforces strict UTF-8; raw arbitrary bytes in JSON strings fail. This matters for legacy P2P/internal blobs that happen to be stored through JSON — those should already be on ZAP.
json.MarshalWritedoes NOT append a trailing\n(v1NewEncoder.Encodedid). Adjust HTTP-handler test fixtures accordingly.
Last Updated: 2026-06-06