Files
node/LLM.md
T
zeekay 011c3df6bd chore(node): remove AI-slop write-ups and stale residue
- LAUNCH_CHECKLIST.md: pre-launch plan for v1.24.11, 154/154 boxes
  unchecked, zero references; superseded by LLM.md/CHANGELOG.md/RELEASE.md
  and NETWORKS.yaml + genesis/configs for the chain-ID/port tables.
- rename_app.sh, replace_imports.sh: spent one-off sed migrations. Both
  are now actively harmful — replace_imports.sh would rewrite the live
  github.com/luxfi/vm/manager import in vms/manager.go.
- gen_zoo_addr: stray 3.4MB darwin/arm64 build artifact committed at root;
  source preserved at cmd/gen_zoo_addr/gen_zoo_addr.go (.gitignore already
  covers the intended output path).
- .ci-status-check.md, .ci-trigger: dated CI-poke stamps, no workflow reads
  them.

Also removed (untracked): .claude/worktrees/ agent scratch — a 73MB
whole-tree duplicate that polluted every grep. Detached via git worktree
remove; node HEAD cf8e4c6f51 was an ancestor of main, and the nested
lux/evm worktree HEAD 7156c44f6 is release tag v1.104.12, so no work lost.
Deleted the two fully-merged worktree-agent-* branches it left behind.

No Go source, config, manifest, or .github/ file was touched.
2026-07-25 11:48:34 -07:00

41 KiB
Raw Blame History

AI Development Guide

This file provides guidance for AI assistants working with the Lux node codebase.

Repository Overview

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)

Post-E2E-PQ State (current)

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.goSchemeGate.Classify(presented, site) funnels every inbound NodeID through the profile's AcceptsValidatorScheme. Wire-typed NodeID (luxfi/ids TypedNodeID + scheme byte) is the canonical handshake form.
  • vms/txs/auth/policy.goClassicalCompatRegistry + strict-PQ mempool gate. Both platformvm (P-Chain) and avm (X-Chain) mempools refuse classical credentials when the resolved profile has ForbidECDSAContractAuth=true.
  • vms/mldsafx/ — re-exports the consensus mldsafx UTXO feature extension as the node-owned UTXO surface (ML-DSA-65 verify).
  • network/peer PQ handshake — ML-KEM-768 / ML-KEM-1024 KEM + ML-DSA-65 identity (dc906d281b).

Recent significant commits

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

Active versions

  • Repo: v1.26.12 (next bump: v1.26.13).
  • Pinned: consensus v1.23.4+ (needed ValidatorSchemeID), crypto v1.18.5, ids v1.2.9 (will move to v1.2.10 in next bump for TypedNodeID), genesis v1.9.6.

Cross-repo dependencies

  • luxfi/consensus → profile + auth + zchain types
  • luxfi/crypto → ML-DSA / ML-KEM / SLH-DSA primitives
  • luxfi/genesis → genesis-pinned profile (Resolve at load)
  • luxfi/idsTypedNodeID wire form (consumed at handshake)
  • luxfi/geth → EVM (for vm.SetActiveSecurityProfile install point)

Where to look for X

  • Profile resolve at boot: node/node.go:initSecurityProfile
  • Profile RPC + REST + metrics: service/security/
    • JSON-RPC namespace: security at POST /v1/security (methods securityProfile, blockSecurity)
    • REST sidecars: GET /v1/security/profile, GET /v1/security/block/{n}
    • Prometheus gauges: /v1/metrics under the security_* family
  • 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/

Open follow-ups

  • 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.SetActiveSecurityProfile install point exists in luxfi/geth/core/vm but EVM-side contract-auth refusal still needs a chain-bootstrap call (F102 wiring closes the consensus side; geth-side hookup is the remaining tail).

FeePolicy — canonical user-tx fee gate

Topology + UTXO ownership + cross-chain fee model are normatively specified by LP-0130 (Chain Topology, UTXO Ownership, and Fee Model). Read that LP before touching any VM's fee/settlement path or any cross-chain import/export flow. In particular:

  • Only P and X are canonical UTXO state machines (LP-0130 §2).
  • X is the money rail; P is the staking/reward rail; LUX is the fee currency everywhere (LP-0130 §3, §5).
  • Q-Chain has no user-payable blockspace — finality is a validator obligation paid via P (LP-0130 §6). quantumvm MUST use NoUserTxPolicy{} — enforced in chains/quantumvm/feegate.go as of 2026-07-03.
  • M-Chain fees are service fees deducted from the originating chain's fee pool, not a user M-balance (LP-0130 §7). mpcvm already runs NoUserTxPolicy{} — correct.
  • B-Chain fees are deducted from the bridged amount (LP-0130 §8).
  • Every non-P/X chain settles worker rewards to X (asset payouts) or P (staker rewards) via epoch fee roots reconciled at Q finality (LP-0130 §4, §11).
  • Σ-escrow invariant (LP-0130 I-8): Σ non-P/X fee balances == Σ X-side fee escrow at every Q checkpoint. Drift is a finality-blocking fault.

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.

Policy choice per VM

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, ...} (deducted from bridged amount, LP-0130 §8)
quantumvm Q-Chain service-only (LP-0130 §6) NoUserTxPolicy{} — validator obligation, no user-payable blockspace
identityvm I-Chain user-tx FlatPolicy{Fee: MinTxFeeFloor, ...}
mpcvm M-Chain service-only (LP-0130 §7) NoUserTxPolicy{} — fees pulled from originating chain's fee pool
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); balance is X-imported LUX (LP-0130 §10)
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.

Wiring contract

  1. VM struct holds feePolicy fee.Policy (and networkID uint32).
  2. Initialize sets feePolicy = fee.FlatPolicy{...} (or fee.NoUserTxPolicy{} for service-only) from init.Runtime.NetworkID and calls fee.Validate(vm.feePolicy) — refuses zero-fee user-facing chains at boot, before any block is accepted.
  3. The canonical user-tx admission entry (e.g. SubmitTx, IssueTx, InitiateBridgeTransfer, mutating service RPCs) calls policy.ValidateFee(paid, asset) BEFORE mempool insert.
  4. Consensus-internal paths (engine→VM block delivery, replay, internal tx emission) bypass the fee gate — the policy gates only the user-submitted entrypoint.

Where the gates live

  • 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)

C-Chain tx-fee routing — RewardManager to DAO Safe (P-Chain: NO CHANGE)

Owner tokenomics pivoted: 100% of C-Chain tx fees accrue to the chain's DAO Gov Safe via the existing rewardmanager precompile (C-Chain only). This needs no P-Chain change — routing is GetCoinbaseAt → reward address on the C-Chain. See ~/work/lux/evm/LLM.md → "C-Chain Tx-Fee Routing — RewardManager". The P-Chain feeRewardPool fold-in below is NOT built (design-only, superseded); no vms/platformvm/state change was made.

Superseded design — 50/50 burn + P-Chain staking-reward fold (dormant option)

If the DAO ever chooses an in-protocol 50/50 split, the C-Chain half exists (dormant, FeeSplitTimestamp gated off) and the P-Chain fold-in would be: system-triggered epoch export of the C-Chain vault C→P → persisted feeRewardPool in vms/platformvm/state (mirror the accruedFees singleton, upgrade-safe) → pro-rata payout at vms/platformvm/txs/executor/proposal_tx_executor.go rewardValidatorTx (~line 285), unified into PotentialReward, NO second mint → decrement currentSupply by the epoch burn. Model R1 (move-not-mint), conservation-exact; R2 (burn+re-mint) rejected.

v1.36.12 fleet rollout — durable rejoin fix + RewardManager→DAO Safe (IN PROGRESS 2026-07-15)

Rolling the durable rejoin fix (node 63f61429d1) across all Lux nets, gated devnet→testnet→mainnet, + activating RewardManager fees. Two hard facts were found on the devnet canary that change the naive "swap image" plan:

BLOCKER (fixed in v1.36.12): published node:v1.36.11 (digest c3cf92a6) cannot run ANY EVM chain. Its baked VM plugins were built against a stale luxfi/api: C-Chain EVM from luxfi/evm@v1.104.8 and D-Chain dexvm from luxfi/dex@v1.5.15 both resolve api v1.0.15; the node pins api v1.0.16, which APPENDED InitializeResponse.Capabilities (Quasar-export handshake, api 1f2dc5a). Node decodes the field, stale plugins never encode it → vms/rpcchainvm/zap/client.go fails every EVM Initialize with zap decode initialize response: unexpected EOF. Native VMs (P/X/Q) unaffected. Fix = v1.36.12 (this repo, tag pushed, ARC docker build run 29381442539): EVM_VERSION v1.104.8→v1.104.9, force api@v1.0.16 in the dexvm build stage; CHAINS_REF=v1.7.6 was already v1.0.16. Node binary unchanged (still the durable fix). api bump is code-free for plugins (chains v1.7.4→v1.7.5 adopted it go.mod-only). Roll v1.36.12, NOT v1.36.11. (Also: api 1f2dc5a added Capabilities WITHOUT bumping version.RPCChainVMProtocol (42) → skew is invisible at handshake, only fails at Initialize decode. Consider bumping the protocol next api-wire change so skews fail fast.)

MIGRATION: v1.36.2→v1.36.x is a P-Chain codec change (linearcodec→ZAP-native), one-time DB wipe + re-bootstrap. v1.36.11/12 cannot read a v1.36.2 P-Chain zapdb (loadMetadata: feeState: zap: invalid magic bytes; state_commit.go:116 "database must be wiped"). Recovery = wipe /data/db+/data/chainData, re-bootstrap from peers. Cross-version bootstrap (v1.36.11 node ← v1.36.2 peers) is PROVEN working (devnet luxd-1: P/X re-bootstrapped from the 4 v1.36.2 peers). Devnet startup got a marker-gated one-time wipe (/data/.zap-native-migrated): absent→wipe+set marker, present→NO wipe (the durable-fix no-wipe restart path). mainnet/testnet/zoo use startup.sh which already has .wipe-cchain (C-Chain only) + .allow-bootstrap (flips skip-bootstrap=false + EVM state-sync); for the codec migration the P-Chain zapdb (/data/db) must also be cleared. Mainnet C-Chain is 1.08M blocks → MUST enable EVM state-sync for the re-sync (full replay is too slow); native VMs are tiny.

Durable fix (63f61429d1): discriminator for keeping the staked beacon set is SYBIL-PROTECTION, not --skip-bootstrap. So a behind validator on a sybil-protected net catches up from peers even with --skip-bootstrap=true (which prod hardcodes), no wipe. Devnet added --skip-bootstrap=true to the inline cmd to exercise this.

RewardManager (C-Chain precompile, per-net cchain-upgrade.json → append one precompileUpgrades entry, dated blockTimestamp): proven testnet shape is {"rewardManagerConfig":{"blockTimestamp":<ts>,"adminAddresses":["<admin>"], "initialRewardConfig":{"rewardAddress":"<reward>"}}}. Reward addr = coinbase; 100% fees land there, blackhole 0x0100…00 goes flat. Addresses: testnet ALREADY LIVE (reward 0xEAbCC110fAcBfebabC66Ad6f9E7B67288e720B59, admin 0x9011…94714); mainnet reward+admin = DAO Gov Safe 0x8E29b816c6C35b13cE1ff68D33E245C2bda8ac3D; zoo reward 0x229599f227231d8C90fcF1a78589F5DC4b7A6962; devnet reward 0x8d5081153aE1cfb41f5c932fe0b6Beb7E159cF84 (idx2), admin 0x9011…94714 (idx0). Source ConfigMaps: devnet luxd-chain-upgrades/cchain-upgrade.json; mainnet+testnet luxd-startup/cchain-upgrade.json; zoo zood-mv-genesis/upgrade.json (--upgrade-file).

Rollout levers (lux-operator is scaled 0/0 — sts/cm are the live source of truth; CRs are STALE, do not scale operator up mid-roll): ports devnet 9650 / testnet 9640 / mainnet 9630 / zoo 9630; RPC path /v1/bc/C/rpc; container luxd (zood on zoo). Devnet uses an inline generated cmd; testnet/mainnet/zoo use /scripts/startup.sh. Zoo zood-mv trap: RollingUpdate + hardcoded --skip-bootstrap=true with NO .allow-bootstrap gate → switch to OnDelete BEFORE rolling. Lux sts are OnDelete. Master funded key = LUX_MNEMONIC (secret lux-deployer) idx0 0x9011…94714; genesis/cmd/derivekey -mnemonic "$M" (path m/44'/9000'/0'/0/i, CGO_ENABLED=0).

Per-node roll protocol (ALL nets, one at a time, NEVER 2 mainnet down — quorum 4/5): set sts image v1.36.12 (+ rewardManager cm edit) → delete ONE pod → WAIT until it is back at TIP HEIGHT matching the others (NOT pod-Ready; a wedged node false-reports Ready) AND C-Chain serves RPC → only then the next. If any node fails to return to tip, STOP that net and report.

State at pause: v1.36.12 tag pushed + ARC build dispatched (run 29381442539). Devnet sts = v1.36.11 + skip-bootstrap + wipe-marker; luxd-1 migrated (P/X up on v1.36.11, C-Chain down = the plugin bug → will heal on v1.36.12); luxd-0/2/3/4 still v1.36.2 healthy (devnet C-Chain 4/5). Nothing rolled on testnet/mainnet/zoo. NEXT: when v1.36.12 image is ready → set devnet sts image v1.36.12, delete luxd-1, confirm C-Chain inits + reaches tip; then finish devnet (durable-fix proof + RewardManager), then gated testnet→mainnet→zoo.

Essential Commands

Release & build (canonical) — via platform.hanzo.ai, NOT GitHub Actions

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 to s3://lux-plugins-<env>/<pluginset>/ (operator pluginSource). The .github/workflows/* build/release workflows are retired (RELEASE.md §Retire).

Building (local dev only)

# Build node binary
./scripts/run_task.sh build
# Output: ./build/luxd

# Build specific components
go build -o luxd ./app

Testing

# Run all tests
go test ./... -count=1

# Run specific package
go test ./vms/platformvm/state -count=1

# With race detection
go test -race ./...

Code Generation

# Generate mocks
go generate ./...

# Regenerate protobuf
./scripts/run_task.sh generate-protobuf

Running

# Mainnet
./build/luxd

# Testnet
./build/luxd --network-id=testnet

# Local network
lux network start

Architecture

Multi-Chain Design

Primary 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

Consensus Layer

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)

Virtual Machines

Located in /vms/:

  • platformvm: Staking, validation, network management
  • xvm: Asset transfers, UTXO model
  • dexvm: DEX with order book, perpetuals, AMM
  • mpcvm: 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

Key Interfaces

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]
}

Package Dependencies

CRITICAL: Use Lux packages only

  • 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

Import Aliasing

Avoid conflicts with consensus packages:

import (
    platformblock "github.com/luxfi/node/vms/platformvm/block"
    consensusblock "github.com/luxfi/consensus/engine/chain"
)

Token Denomination

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.

Key Technical Decisions

Genesis Architecture

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)

CGO Dependencies

These require CGO for full functionality (graceful fallback when disabled):

  • consensus/quasar - GPU NTT acceleration
  • vms/mpcvm/fhe - GPU FHE operations
  • x/blockdb - zstd compression

FHE (Fully Homomorphic Encryption)

Located in vms/mpcvm/fhe/:

  • Uses github.com/luxfi/lattice/multiparty for 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 Transport (Zero-Copy App Proto)

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 tags

Key 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-side p2p.Sender over ZAP
  • vms/rpcchainvm/zap/ — ChainVM client/server over ZAP
  • vms/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-neutral Service wrapping database.Database
    • zap_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)

RNS Transport (Reticulum Network Stack)

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

Endpoint Types

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")

Key Files

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

Configuration

# ~/.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 peers

Post-Quantum Cryptography (Hybrid Mode)

RNS transport supports hybrid post-quantum cryptography combining classical algorithms with NIST-standardized post-quantum primitives (TLS 1.3-like approach).

Cryptographic Suite

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 - -

Forward Secrecy

  • 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

Wire Format Sizes

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

Backward Compatibility

  • 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: true rejects classical peers

SchemeGate — cross-axis NodeIDScheme enforcement

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: ActivationHeight is the block at which a strict-PQ chain refuses any non-PQ NodeIDScheme byte at every height under the forward-only PQ policy. The classical secp256k1 (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.

Testing PQ Forward Secrecy

# 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)

Common Gotchas

1. P2P Sender Interface

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.

2. Chain Tracking

Nodes don't automatically track chains. Use:

--track-chains=<ChainID>

Or create config: ~/.lux/runs/.../node*/chainConfigs/<ChainID>.json

3. Genesis blobSchedule

Mainnet genesis requires Cancun fork config:

"blobSchedule": {
  "cancun": {
    "max": 6,
    "target": 3,
    "baseFeeUpdateFraction": 3338477
  }
}

4. Network Snapshots

CLI creates new directories on restart. Use snapshots:

lux network save --snapshot-name <name>
lux network start --snapshot-name <name>

5. EIP-3860 Historic Blocks

For importing pre-merge blocks, Shanghai must be active based on ShanghaiTime, not merge status.

6. Genesis Hash Mismatch on Restart

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.

7. VM Config Format Mismatch

Problem: "failed to parse config: unknown codec version" for T-Chain (ThresholdVM) or Z-Chain (ZKVM) in dev mode.

Cause: Two issues:

  1. Genesis builder passes JSON config ({"version":1,"message":"..."}) to VMs that expect binary codec format
  2. 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 of getGenesis() which returns JSON
  • chains/manager.go: injectAutominingConfig only injects for EVMID, skipping binary-codec VMs

Alternative: Use --genesis-raw-bytes flag to pass base64-encoded pre-built genesis bytes directly.

8. vms/components/lux vs luxfi/utxo (parallel UTXO types)

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.

File Locations

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 Order

  1. Build node: cd ~/work/lux/node && go build -o /tmp/luxd ./main
  2. Install: cp /tmp/luxd ~/.lux/bin/luxd/luxdv1.21.0/luxd
  3. Build EVM: cd ~/work/lux/evm && go build -o ~/.lux/plugins/<VMID> ./plugin
  4. 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

Security Notes

Mainnet Readiness (2025-12-31)

  • 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

11. P-Chain Block Sync (isMissingContextError "not found")

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 state

Effect: 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.

Known CGO Stubs

When CGO disabled, these use CPU fallbacks:

  • consensus/quasar/gpu_ntt_nocgo.go
  • vms/mpcvm/fhe/gpu_fhe_nocgo.go
  • vms/zkvm/accel/accel_mlx.go

8. ZAP CreateHandlers for VM HTTP Endpoints

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:

  1. Sends MsgCreateHandlers via ZAP wire protocol to the VM
  2. Receives CreateHandlersResponse with list of handlers (prefix + server address)
  3. Creates httputil.NewSingleHostReverseProxy for each handler
  4. Returns map[string]http.Handler for 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"}

9. Root "/" Endpoint Handler

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

10. BLS Key Not Loaded into Validators Manager

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:

  1. Network layer might pre-populate validators (without BLS keys) before state initialization
  2. When initValidatorSets() runs, it sees validators exist and skips adding them with proper BLS keys
  3. 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 keys

Verification:

curl -s http://localhost:9650/v1/health | jq '.checks.bls'
# Should show: "message": "node has the correct BLS key"

Benchmark Results (Single Node)

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=5

JSON rule — json/v2 at HTTP boundary only; ZAP for all internal data

Encoding boundaries are one-way and explicit:

  • External (HTTP / JSON-RPC API)github.com/go-json-experiment/json (v2). Never encoding/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]byte field with no MarshalJSON ⇒ v2 marshals as base64 string, v1 marshalled as JSON array of byte numbers. Add MarshalJSON on 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) exposes FormatDurationAsNano(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.MarshalWrite does NOT append a trailing \n (v1 NewEncoder.Encode did). Adjust HTTP-handler test fixtures accordingly.

Housekeeping

Removed 6 generated write-ups / stale root artifacts (LAUNCH_CHECKLIST.md, rename_app.sh, replace_imports.sh, gen_zoo_addr binary, .ci-status-check.md, .ci-trigger) plus the 73MB .claude/worktrees/ agent scratch tree. Release and launch state live in this file, CHANGELOG.md, RELEASE.md; chain IDs/ports in ~/work/lux/universe/NETWORKS.yaml and ~/work/lux/genesis/configs/.


Last Updated: 2026-06-06