`go build` (and CGO_ENABLED=1) now build with zero external setup — no
luxcpp, no pkg-config, no install step — given accel >= v1.2.1 (native
HQC made opt-in). GPU acceleration (Metal/CUDA) is a runtime dlopen
plugin selected via LUX_GPU_BACKEND/LUX_GPU_BACKEND_PATH; the same binary
runs with or without it. Replaces the 3-tier build/install dance.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TxKindBaseFull = 11 → BaseTxFull
TxKindAddPermissionlessValidator = 12 → AddPermissionlessValidatorTx
TxKindImport = 13 → ImportTx
TxKindExport = 14 → ExportTx
TxKindCreateChain = 15 → CreateChainTx
Sizes (fixed section, schema v3):
BaseTxFull = 85
ImportTx = 125
ExportTx = 125
AddPermissionlessValidatorTx = 381
CreateChainTx = 221
Design highlights:
- BaseTxFull is the real P-chain spending envelope (Outs + Ins +
Credentials + 2 shared arrays + Memo). The batch-2 BaseTx
(TxKindBase) remains as the minimal metadata envelope for places
that need only {NetworkID, BlockchainID, Memo} without spending
state. Both kinds are first-class; they are NOT alternatives.
- Import / Export use a SINGLE combined Ins (or Outs) list with a
header marker (ImportedInsStart/Count or ExportedOutsStart/Count)
identifying the cross-chain slice. This collapses what would have
been two parallel sig-index arrays into one shared array — fewer
pointer pairs, no rebase bookkeeping. The slice-based indexing is
byte-identical for the imported/exported half because they
reference the same shared array.
- AddPermissionlessValidatorTx carries two single-address Owner stubs
(validation rewards, delegation rewards) inline in the fixed
section. The same OwnerStub type underlies CreateChainTx.Owner.
Multi-address Owner ships in batch 4 along with the AddressList
primitive; current callers needing multi-addr flow through the
legacy codec gate.
- CreateChainTx embeds a 32-byte WarpMessageHash (sha256 of the
originating Warp commit; zero when the chain is being created
directly without a cross-network commit). The Warp message BODY
is NOT embedded — it lives in the signed Warp envelope outside
this tx. Hash-only embedding keeps the unsigned-tx bytes stable
and the Warp envelope separately verifiable.
All five tx types follow the v3 invariant: TxKind@0, Wrap* rejects
mismatched discriminator with ErrWrongTxKind. Cross-confusion test
expansion verifies the new tx types in pairwise rejection (7
additional scenarios cover the batch 1+2+3 surface).
Deferred to batch 4 (no new primitives needed): the remaining ~18
classical platformvm tx types (CreateNetwork, AddDelegator,
AddPermissionlessDelegator, etc.) reuse the existing batch-3
primitives; their landings are mechanical.
go test -race ./vms/platformvm/txs/zap_native/...
ok github.com/luxfi/node/vms/platformvm/txs/zap_native 1.488s
Five primitives. Each is fixed-stride at the entry level; variable
per-entry bytes/sub-list payloads live in shared sibling fields on
the parent tx, indexed by (start, count). This keeps List.At(i)
zero-allocation while supporting unbounded per-entry payload sizes.
OutputList stride 96 → TransferableOutput
InputList stride 88 → TransferableInput + shared SigIndicesArray
CredentialList stride 16 → Credential + shared SignatureArray
WarpMessage size 40 → embedded {SourceNetwork, Payload}
EvidenceList stride 48 → EvidenceEntry + shared MessageBlobs/SignatureBlobs
Design choices:
- Stride is the entry-count semantics for SetList; the byte count from
ListBuilder.Finish() is discarded. List.Object(i, stride) does the
branch-free arithmetic.
- Variable per-entry data goes into a SIBLING field on the parent tx
(not into the entry's stride): InputList → SigIndicesArray;
CredentialList → SignatureArray; EvidenceList → MessageBlobs +
SignatureBlobs. Each entry references its slice via (start, count).
This pattern lets entries stay fixed-stride and accessors stay
zero-allocation. Multi-input-shared signature arrays also enable
signature deduplication when adjacent inputs share signers.
- Forward-only relOffsets per the F1 contract: SetBytes-backed payload
fields (WarpMessage.Payload, EvidenceList parent's MessageBlobs/
SignatureBlobs) flow through the F1-fixed zap.Object.Bytes which
rejects negative bit-patterns.
- Read-only contracts on every []byte / by-value accessor are
documented with the canonical defensive-copy idiom
(append([]byte(nil), m...)) per AT1.
- Multi-address Owner is NOT yet supported at the wire layer; v3
OutputList carries the single-address stub identical to
TransferChainOwnershipTx. Multi-address callers still flow through
the legacy codec gate behind LUXD_ENABLE_LEGACY_CODEC. The
AddressList primitive ships in batch 4 (defer).
- Multi-signature credentials handled via the SignatureArray slice
semantics — one Credential, N sigs. Post-quantum credentials
(ML-DSA) are NOT YET on the v3 path; they keep flowing through
legacy until their dedicated PQ Credential schema lands.
Round-trip tests (batch3_test.go) cover all five primitives + the
empty-list/null-pointer fallback path. go test passes; race-free.
Updated bench numbers for v3 batch-2 (the +1-byte TxKind tax):
Parse geomean speedup vs legacy: 7.09× (was 8.39× pre-v3)
Build geomean speedup vs legacy: 1.35× (build path was always
closer to parity; structure-allocator dominates at small tx sizes)
Phase A response to Red's batch-2 review:
F2 (MEDIUM, cross-type confusion) — schema-bump v2→v3. Every fixed
section now carries a TxKind uint8 at offset 0; every other field
shifts by +1 byte. Wrap*Tx reads TxKind first and returns
ErrWrongTxKind on mismatch. Constructors write the kind
unconditionally. Closes the gap where an AdvanceTimeTx buffer wrapped
as a BaseTx returned garbage-but-deterministic field reads.
TxKind enum (dense, 0 reserved):
1=AdvanceTime 2=RewardValidator 3=SetL1ValidatorWeight
4=IncreaseL1ValidatorBalance 5=DisableL1Validator 6=Base
7=RegisterL1Validator 8=SlashValidator
9=TransferChainOwnership 10=RemoveChainValidator
v3 sizes (each +1 vs v2 from the TxKind byte):
AdvanceTimeTx = 9
RewardValidatorTx = 33
SetL1ValidatorWeightTx = 49
IncreaseL1ValidatorBalanceTx = 41
DisableL1ValidatorTx = 33
BaseTx = 45
RegisterL1ValidatorTx = 217
SlashValidatorTx = 57
TransferChainOwnershipTx = 69 (no natural-alignment padding; reads are alignment-tolerant)
RemoveChainValidatorTx = 53
F1 (MEDIUM, memo malleability) — closed at the luxfi/zap wire layer
in v0.6.1 (negative relOffset rejected in Object.Bytes). Bumped node's
go.mod replace: zap v0.2.0 → v0.6.1. TestRed_V2 now confirms the
defense via the nil-Memo branch.
F4 (INFO, doc bugs) — RegisterL1ValidatorTx comment now correctly
declares size 217; TransferChainOwnershipTx now correctly declares
size 69. Both sizes flow from offset arithmetic — no magic numbers.
AT1 (accepted tradeoff, memo aliasing) — BaseTx.Memo() docstring
escalated: READ-ONLY contract + the canonical defensive-copy idiom
(append([]byte(nil), m...)). Same pattern documented for every
variable-length accessor going forward.
Brand cleanup: SlashValidatorTx.Subnet() → Network() and
RemoveChainValidatorTx.Subnet() → Network(). Zero `Subnet*` symbols
in batch-2 code path. Tests updated.
V14 security test now exhaustively covers cross-confusion: 10
pairings of {valid-tx-buf, wrong-Wrap} + reserved TxKind=0 — all
reject with ErrWrongTxKind. The other 19 Red vectors continue to
pass under v3.
go test -race ./vms/platformvm/txs/zap_native/...
ok github.com/luxfi/node/vms/platformvm/txs/zap_native 1.446s
Coordinated with luxfi/zap@v0.6.1 (F1 fix).
Post-coreth networks (test+dev primary that bake only P + EVM genesis chains)
run in P-only mode where the X alias is not registered. The wallet's
FetchState now degrades the X-Chain context to a sentinel ids.Empty
BlockchainID instead of erroring, and skips the X-chain entry from the
chain-pair UTXO scan when X is unavailable.
Required to drive IssueCreateChainTx via the canonical primary wallet
against the fresh test+dev primaries (closes universe #168).
Real-workload benchmark suite produced by the scientist agent. Production-
realistic measurements on Apple M1 Max (Go 1.24).
Harness (8 files + results + reproduce docs):
bench/fixtures.go — realistic per-type tx fixtures
bench/parse_bench_test.go — per-type Parse: Legacy vs ZAP
bench/build_bench_test.go — per-type Build
bench/field_bench_test.go — field access (single + 1M batch)
bench/workload_bench_test.go — 1000-tx mempool + 200-block parse + dispatcher
bench/alloc_bench_test.go — 5s sustained-parse GC pressure
bench/disable_legacy_test.go — LUXD_ENABLE_LEGACY_CODEC gate verification
bench/README.md — reproduce + capture procedures
bench/testdata/README.md — captures notes
bench_results/RESULTS.md — full numbers + honest caveats + CPU profile
Headline numbers (vs linearcodec):
Parse AdvanceTimeTx : 37x faster (1940 ns -> 52.5 ns), 20x less mem
Build AdvanceTimeTx : 5.2x faster
Sustained 5s parse loop : 18.5x throughput (777k -> 14.4M parses/sec)
Cross-type mean : 5.6x parse, 1.6x build
Allocations (parse, build) : always 3->1 and 4->2
CPU profile:
Legacy: ~50% in reflectcodec.{marshal,unmarshal} reflection walk,
~30% in runtime.{madvise,kevent} from per-field alloc GC pressure
ZAP: elimination of both surfaces; offset arithmetic + 1 alloc per parse
Honest caveats (verbatim from scientist report):
- Only 5/33 tx types have native paths today; mempool mix workload
compresses to ~1.1x lift until BaseTx + AddPermissionless* native ship
- Field access: single uint64 read is 2.1x slower (offset deref vs struct
field), break-even at ~3,560 reads per parse; mainnet validators do
~10x per tx so ZAP still wins by orders of magnitude in the real regime
- darwin/arm64 only — re-run on linux/amd64 before quoting production-
binding numbers
- LUXD_ENABLE_LEGACY_CODEC gate verified at zap_native surface (subprocess
test passes); NOT yet wired into platformvm/txs.Parse — would break
byte-preserving v0 read for validators bootstrapping pre-activation
history. Gate enforcement lives at the future wire dispatcher.
Recommendation (CTO direction): continue the migration. Architecture works
as designed. Next priority: BaseTx + AddPermissionless* native paths — the
two highest-weight tx types in the modal mainnet mempool mix.
txs/tx.go is unmodified — byte-preserving v0->TxID migration invariant
preserved per existing codec.go CodecVersionV0/V1 framework.
Reproduce: cd ~/work/lux/node && GOWORK=off go test -bench=. -benchmem ./vms/platformvm/txs/bench/...
LUXD_DISABLE_LEGACY_CODEC → LUXD_ENABLE_LEGACY_CODEC (per user 2026-06-02
'should be LUXD_ENABLE_LEGACY_CODE=1 to turn it on').
Default: native ZAP for every read + write. codec.Codec.Marshal/Unmarshal
gone from hot path. Fresh deployments + post-activation production get a
smaller, faster binary with no legacy code reachable.
Operators with pre-activation history that needs reading set
LUXD_ENABLE_LEGACY_CODEC=1 to opt in to backward-compat. Without it, legacy
bytes return ErrLegacyCodecDisabled.
Tests updated for the inverted default; pass clean.
Pulls in plugin/evm StateScheme fix that unblocks fresh L2 EVM chain
creation on lux-mainnet (hanzo, zoo, spc, pars), where the plugin was
panicking with "panic in eth.New: triedb parent [<EmptyRootHash>] layer
missing" because eth/backend.go inherited geth's path-by-default for
empty DBs while the VM hard-refuses path mode upfront.
See luxfi/evm v0.18.18 commit 1dea806f8.
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).
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.
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).
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.
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).
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/...
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.
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.
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 (455b994b4c 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/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 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).
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.
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.
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.
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.
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
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.
Closes the codec-version trap that surfaced when the v1.23.x ("Apricot/Banff") tx + block layout was rip-replaced in b1e265a708 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
Picks up ChainConsensus.ForceAccept — the consensus-level counterpart
to ForcePreference. Together with the engine.finalizeOwnProposal helper,
this lets a proposer self-finalize its own block at proposal time when
peer Chits do not arrive in time, closing the CreateChainTx stall
observed on devnet under low validator counts.
- ChainConsensus.ForceAccept(blockID) — direct accept bypassing alpha/K
quorum, guarded by engine path (IsOwnProposal=true) and idempotent.
- Engine path uses ForceAccept after ForcePreference on own proposals
so the proposing node commits locally and other validators converge
via the next poll round.
Test delta: pre-existing fee/static_calculator L1Tx parse failures on
main are unaffected; consensus, vms/platformvm/{block,blockmock,state,
warp,...}, and genesis/builder all pass with race -short.
Add TestCanonicalGenesisFixtureParses — loads the canonical mainnet
genesis.json (genesis v1.12.19 evmAddr/utxoAddr field names) through
genesiscfg.GetConfigFile and asserts EVMAddr/UTXOAddr decode to
non-zero ids.ShortID values.
This is the "have we adopted the rename" gate — if the loader
silently swallows the new field names (e.g. via a regression to
ethAddr/luxAddr struct tags), every allocation Address comes back
as ShortEmpty and the assertion catches it before that ships.
Test is host-path aware: skips when ~/work/lux/genesis is not on
disk (CI without sibling checkout), runs when it is.
luxfi/consensus v1.25.0 → v1.25.1
Proposer-self-accept gap on nova multi-node finality: the proposer of
block B was never marking B locally accepted because manager.applyQbit
re-derived peer Chits via a second blk.Verify() call which most VMs
(notably PlatformVM CreateChainTx) are not idempotent under, flipping
every Chits into synthetic Accept=false. Fix tags the proposer's own
pending entry with IsOwnProposal=true and short-circuits the re-verify
in handleVote — peer Chits now count as the genuine Accepts they are.
luxfi/genesis v1.12.15 → v1.12.19
Decomplected: pkg/genesis/security/ is a nested module (own go.mod,
tagged pkg/genesis/security/v1.12.19) that owns SecurityProfile
verification — the only file in luxfi/genesis that imports
luxfi/consensus. The rest of pkg/genesis stays consensus-dep-free.
API change: pin.Resolve() → genesissecurity.ResolveProfile(pin).
ErrSecurityProfileHashMismatch and ErrSecurityProfileInvalidID also
moved to the security submodule. Updated node/node.go and
node/security_profile_test.go to match.
Build: GOWORK=off go build ./... clean (linker warnings about accel
static lib path are pre-existing host-config noise, not a regression).
Vet: GOWORK=off go vet ./... clean (the cevm_e2e_test.go
sync/atomic.Bool copy warning is pre-existing on v1.27.24 main).
Tests: ./consensus/... pass, ./genesis/... pass, ./node/...
TestApplySecurityProfile_* pass. ./vms/platformvm/txs/fee L1-validator
failures are pre-existing on v1.27.24 main and unchanged.
Devnet fixture (~/work/lux/genesis/configs/devnet/genesis.json) parses
clean: networkID=3, 5 initialStakers, canonical evmAddr/utxoAddr only.
The field is the primary network's UTXO fee asset (P+X), not
X-chain-specific. P-chain CreateChainTx / AddChainValidatorTx and
X-chain transfers all burn it for fees. Same number on P and X by
construction; the name should reflect the function, not the chain.
Renames applied across:
- wallet/chain/{p,x}/builder.Context.UTXOAssetID
- wallet/chain/x/builder.Backend.UTXOAssetID() trait method
- node/config: resolveUTXOAssetID + nodeConfig.UTXOAssetID
- vms/platformvm + vms/xvm + chains/manager consumers
- examples (deploy-chains, get-p-chain-balance, ...)
Includes local replaces for luxfi/{runtime,consensus} pending their
release tags landing. Drop after upstream tags ship.
Companion PRs: luxfi/runtime#2, luxfi/consensus#11. Downstream
consumers (sdk, cli, genesis, liquidity) follow.
luxfi/proto#6 (merged) renames SubnetUptime → ChainUptime in
node/zap/p2p, completing the no-subnet vocabulary rule from
node#116. The local re-export in proto/p2p/p2p_zap.go was already
written against the new upstream name, breaking v1.27.22 builds:
proto/p2p/p2p_zap.go:42:32: undefined: p2p.ChainUptime
Tagging v1.0.2 against the merged proto main and bumping the module
pin unblocks the build with zero code changes — the alias line is
unchanged because upstream already matches.
Build: clean. Test: pre-existing platformvm/txs/fee L1-validator
failures unchanged (not introduced here).
Pulls in luxfi/genesis v1.12.15 which strips the dual-name
luxAddr/ethAddr alias shim. AllocationJSON now emits and accepts only
the canonical evmAddr/utxoAddr field pair.
docker-entrypoint.sh genesis templates: switch luxAddr → utxoAddr +
ethAddr → evmAddr to match.
This is the cascade step needed before luxd v1.23.43 ships — the runtime
parser at v1.23.31 (current devnet image) reads ethAddr/luxAddr; v1.23.42
already reads evmAddr/utxoAddr internally; v1.23.43 (this commit + tag)
drops every back-compat alias both ways.
Tests: genesis/builder, vms/platformvm/genesis pass. End-to-end parse
of configs/devnet/genesis.json with canonical names: 1005 allocations,
5 initial stakers (matches 5-pod sybil quorum).
Cluster bump path: lux-devnet (1.23.31 → v1.23.43); testnet/mainnet
remain on v1.23.31 until coordinated migration.
Co-authored-by: Hanzo AI <dev@hanzo.ai>
Per canonical rule: Lux is its own thing, fuji isn't ours. Sweep
removes positive references to the Avalanche testnet name from
comments, function names, test fixtures, and example data. Where
defensive code rejects non-Lux HRPs (lux/genesis), the test input
swapped from 'P-fuji1...' to 'P-avax1...' so the rejection behavior
still proves out without naming fuji as a thing.
Build clean.
Per canonical rule: Lux is its own thing, fuji isn't ours. Sweep
removes positive references to the Avalanche testnet name from
comments, function names, test fixtures, and example data. Where
defensive code rejects non-Lux HRPs (lux/genesis), the test input
swapped from 'P-fuji1...' to 'P-avax1...' so the rejection behavior
still proves out without naming fuji as a thing.
Build clean.
* 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.
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.
Per SCALE_STANDARD.md §2 (~/work/hanzo/hips/docs/SCALE_STANDARD.md) —
every Go production Dockerfile that emits JSON to a client builds with
GOEXPERIMENT=jsonv2. Verified on hanzoai/zip's json_bench_test.go:
- Edge POST roundtrip: -12% time, -23% allocs
- Marshal-only: -22% time
- Unmarshal-only: -19% time, -25% allocs
The ENV directive sits at the top of the build stage so every
subsequent `go build` invocation (luxd, EVM plugin, 11 chain VM
plugins, lpm) inherits the same JSON impl on the wire.
The v0.8.40 EVM plugin was built against luxfi/node v1.23.4 — pre-rename
EngineAddressKey ("LUX_VM_RUNTIME_ENGINE_ADDR"). The host (built from
this repo) has been emitting the NEW key ("VM_RUNTIME_ENGINE_ADDR")
since 4ae211d46d (2026-05-15). Every luxd:v1.27.x image embeds a plugin
that reads the OLD key — empty env → no dial-back → C-chain never
bootstraps → all 4 L2 EVMs stay un-bootstrapped indefinitely.
v0.18.14 pins luxfi/node v1.27.6 which carries the rename, so the
plugin built from it reads the same key the host writes. Pairs with
b0a4679314 (host compat shim) — once every cluster pulls an image that
contains this Dockerfile bump, the shim is a no-op.
The EngineAddressKey const renamed from LUX_VM_RUNTIME_ENGINE_ADDR to
VM_RUNTIME_ENGINE_ADDR in 4ae211d46d (2026-05-15). luxd builds from
that commit forward set VM_RUNTIME_ENGINE_ADDR on plugin exec, but the
EVM plugin (luxfi/evm@v0.8.40 → luxfi/node@v1.23.4 → "LUX_VM_..." const)
in the same Docker image still os.Getenv("LUX_VM_...") — empty string,
no dial back, plugin exits, C-chain never bootstraps, all L2 EVMs stay
un-bootstrapped indefinitely on v1.27.x.
Set BOTH env keys until every plugin in /data/plugins has been rebuilt
against a luxfi/node version that has the rename. New const is the
canonical name; legacy const is the compat key (added as
LegacyEngineAddressKey and only emitted when it differs from the new
key, so once luxfi/evm bumps past the rename the extra env var becomes
a no-op and this line can be deleted without runtime impact).
consensus@v1.24.6 calls keyera.Bootstrap(...) with three return values
but its own go.mod still pins corona v0.4.0 (where Bootstrap returns
two). The 3-value signature lives at corona v0.7.x.
Locally we hide this via go.work using the working tree, so it never
shows up. CI builds without go.work and fails compiling
protocol/quasar/grouped_threshold.go.
Add a replace directive pinning corona to v0.7.5 (current latest).
A proper fix is to tag a luxfi/consensus v1.24.7 with corona bumped
in its own go.mod, but luxd is the only consumer of this transitive
mismatch right now — the replace keeps it tight and reversible.
The v1.27.18 diagnostic confirmed luxfi org GH_TOKEN secret resolves to
length=0 at runtime (visibility is set to 'all' but the actual value
appears unset/expired). UNIVERSE_PAT is set at the repo level and has
the same cross-repo read scope we need for private luxfi/* deps. Try
GH_TOKEN first, fall back to UNIVERSE_PAT, fail fast with a clear
error if both are empty.
Also explicitly disable docker/metadata-action's latest=auto flavor so
the :latest floating tag truly never gets emitted (the prior run showed
the action silently injecting it even with no type=raw rule).
The repeated 'terminal prompts disabled' fatal at go mod download in
v1.27.15..v1.27.17 looked like the BuildKit secret mount was producing
an empty /run/secrets/ghtok file. Add a pre-build step that fails fast
if secrets.GH_TOKEN does not resolve at workflow run time (org-secret
visibility=all, but ARC runner ephemeral identities have surprised us
before). The token value is never echoed — only its byte length.
initialSupply was still summing both initialAmount AND unlockSchedule
amounts — even with the UTXO emission fix, the reported supply field
double-counted Avalanche-shaped configs. Match the emission policy:
unlockSchedule wins when non-empty, initialAmount otherwise.
Dockerfile: drop the post-go-mod-download cleanup of the
url.insteadOf git config. The build step at the bottom of the
builder stage also triggers go fetches (resolving build-time
transitive deps), so the rewrite must remain in place. The
throwaway builder stage never ships, so leaving the token in
/etc/gitconfig is fine — only the compiled binary is COPYed into
the runtime image.
Default workflow GITHUB_TOKEN only has read access to the running repo
(luxfi/node), so go mod download fails on private cross-repo deps such
as luxfi/corona. Switch the BuildKit ghtok secret to the org-level
GH_TOKEN (a PAT with org-wide read scope) so the Dockerfile's git
url-rewrite picks up every luxfi/* module the build needs.
Avalanche/legacy genesis JSONs (testnet, mainnet) set initialAmount as the
sum of unlockSchedule (two views of one total). The current builder emits
both an immediately-spendable UTXO for initialAmount AND one UTXO per
unlock entry — double-minting the allocation.
Devnet-style configs set initialAmount with an empty unlockSchedule and
need the single UTXO to be emitted.
One semantic, one UTXO: when unlockSchedule is non-empty, skip the
initialAmount UTXO (the schedule already covers the total). When
unlockSchedule is empty, emit the initialAmount UTXO as before.
Also:
- ci/docker: switch back to self-hosted `lux-build` ARC pool (DOKS hanzo-k8s)
- ci/docker: drop floating `:latest` tag — semver/sha-only policy
Cleanup pass on the C-Chain-disabled future-work comments. No code
change — just the placeholder identifiers now match the canonical
EVMAddress / EVMKeychain naming used by the live KeychainAdapter.
ethAddrs → evmAddrs
FetchEthState → FetchEVMState
Forward-only completion of the strip-aliases work. Examples (18 main.go
files + example_test.go + debug_balance_test.go) used the old
EthKeychain field name on WalletConfig literals. Mass-renamed via
sed across the wallet/ tree to match the canonical EVMKeychain name.
Also: cleaned remaining linter-restored EthKeychain references in
wallet.go (WalletConfig struct field, comments in MakeWallet).
Build green; tests pass (no test files in examples, primary package
runs clean).
Per CLAUDE.md x.x.x+1.
Workspace-wide reconcile to the runtime-data-model axis (EVM) the
state team established earlier. Decomplect by what things ARE:
- EVMAddresses = 20-byte account addresses on EVM-runtime chains
- The internal hash primitive (Keccak256 of secp256k1 pubkey) is
HOW the value is computed, not WHAT it is
wallet/network/primary/wallet.go:
- EVMKeychain interface is canonical (GetByEVM, EVMAddresses)
- KeccakKeychain retained as Deprecated alias
- EthKeychain retained as Deprecated alias
- KeychainAdapter implements all three interfaces; canonical
implementations on EVMKeychain methods, deprecated aliases delegate
wallet/network/primary/common/options.go:
- Options.EVMAddresses() is canonical
- KeccakAddresses() and EthAddresses() Deprecated aliases delegate
- WithCustomEVMAddresses() option helper is canonical
- WithCustomKeccakAddresses() and WithCustomEthAddresses() Deprecated
Deps bumped:
- luxfi/utxo v0.3.1 → v0.3.2 (EVMAddrs canonical, deprecated aliases)
- luxfi/crypto v1.19.13 → v1.19.15 (EVMAddress canonical)
- luxfi/genesis v1.12.11 → v1.12.14 (transitive dep of utxo bump
for crypto/keccak package path)
Per CLAUDE.md x.x.x+1.
Wave 3 of the workspace-wide eth* naming purge. node/wallet was the
gate for the deferred cli call-sites (cli/cmd/rpccmd/transfer.go and
cli/pkg/chain/local.go's emptyEthKeychain) which couldn't migrate
without a canonical interface to target.
wallet/network/primary/wallet.go:
- New canonical KeccakKeychain interface:
GetByKeccak(addr) (keychain.Signer, bool)
KeccakAddresses() set.Set[gethcommon.Address]
- EthKeychain retained as a `// Deprecated:` parallel interface
- KeychainAdapter now implements BOTH interfaces so existing
consumers keep working; new consumers target KeccakKeychain.
- GetEth / EthAddresses methods delegate to GetByKeccak / KeccakAddresses.
wallet/network/primary/common/options.go:
- New canonical Options.KeccakAddresses() method
- New canonical WithCustomKeccakAddresses(...) option helper
- EthAddresses / WithCustomEthAddresses retained as `// Deprecated:`
aliases delegating to the canonical names
Deps bumped:
- luxfi/utxo v0.3.0 → v0.3.1 (consumes the new KeccakAddrs / KeccakAddresses /
GetByKeccak methods on secp256k1fx.Keychain)
- luxfi/crypto stays at v1.19.13 (PrivateKey.KeccakAddress)
This unblocks cli/cmd/rpccmd/transfer.go to call kcAdapter.KeccakAddresses()
and cli/pkg/chain/local.go to implement KeccakKeychain — a future cli
wave (v1.100.5+).
Per CLAUDE.md x.x.x+1.
Final cleanup for the upgrade-name purge (lux/node now runs full
feature-set from genesis under activate-all-implicitly):
- vms/xvm: `var durango = upgrade.Default` -> `var activeUpgrade =
upgrade.Default`. Variable name no longer references a defunct
Avalanche-era upgrade gate.
- wallet/chain/p/builder_test.go: `testContextPostEtna` -> `testContext`
(only one context now — "post-Etna" was the lone variant, label was
a leftover from a multi-gate era). Test names "Post-Etna" /
"Post-Etna with memo" -> "default" / "default with memo".
No behavior change. `go build` + `go vet` clean.
config.UTXOAssetIDFor(networkID) returns a network-id-keyed constant
that's identical across every L1 sharing a primary-network ID. On
sovereign L1s (Liquidity / MLC / VCC / any future tenant) the X-Chain
genesis bakes a different asset (different validator set, different
holder set, different denomination) so the runtime asset ID — the ID
vm.initGenesis assigns to the first GenesisAsset.CreateAssetTx — does
not match the constant.
Every wallet builder that calls platform.getStakingAssetID to populate
pCTX.XAssetID then pays tx fees from UTXOs under an asset the chain
doesn't recognise:
insufficient funds: needs 398 more nLUX (<constant>)
That's the bootstrap failure on Liquidity devnet / testnet / mainnet
after the chainset-via-embedded-genesis fix landed.
Fix: derive the X-Chain native asset ID from the actual genesis bytes
the binary loads at startup. Every load path now goes through
resolveXAssetID(networkID, genesisBytes):
- genesis baked with X-Chain → parse the embedded XVM genesis,
initialize the first GenesisAsset.CreateAssetTx, return its tx.ID()
(the same value vm.initGenesis assigns at runtime).
- genesis is P-only (no X-Chain) → fall back to UTXOAssetIDFor.
Value is unused at runtime, kept for downstream-consumer shape.
- genesis is malformed → error (was previously silently returning
the wrong constant; that's how sovereign L1s ended up shipping
binaries that disagreed with their own chain).
Touched:
- vms/xvm/genesis.go: ParseGenesisBytes + AssetIDFromGenesisBytes.
- vms/xvm/genesis/lux.go: AssetIDFromBytes proxy so the wrapper
package stays the single dependency point.
- genesis/builder/builder.go: FromConfig uses the genesis-derived
ID instead of UTXOAssetIDFor; new XAssetIDFromGenesisBytes
helper for the platform-genesis-bytes path.
- config/config.go: getGenesisData's raw and cached paths now call
resolveXAssetID. FromConfig path inherits the fix automatically.
Tests:
- vms/xvm/genesis/lux_test.go: AssetIDFromBytes is stable, holder-
sensitive, network-id-sensitive, malformed-input-rejecting.
- genesis/builder/builder_p_only_test.go: helper agrees with
FromConfig on sovereign genesis, returns ok=false on P-only,
errors on garbage.
- config/config_test.go: resolveXAssetID returns the genesis-derived
ID for sovereign genesis, UTXOAssetIDFor on P-only, error on
garbage.
No network-id allow-list, no per-network branching, no env-var
overrides. The fix is in the binary's load path; operator workflows
stay byte-identical.
The lux-arm64-runners EKS cluster is unreachable (i/o timeout on
control plane); no Docker builds since the runner pool dropped.
Pinning to ubuntu-latest until ARC is restored. Switch back to
lux-build in a follow-up once the EKS cluster is back up.
luxfi/corona went private; the Dockerfile builder's `go mod download`
was failing with "could not read Username for 'https://github.com'"
on the ARC runner. Adds a BuildKit secret mount (required=false so
local builds without the secret still work when all deps are public)
and rewrites `git config url.insteadOf` to inject the token only for
the download step. Token is unset after to keep the layer clean.
The workflow passes ${{ secrets.GITHUB_TOKEN }} as the `ghtok` secret;
the default GITHUB_TOKEN has repo:read for the runner's repository,
which is sufficient for luxfi/corona since it's in the same org.
Add the per-VM policy table (user-tx vs service-only) and the wiring
contract (Init -> fee.Validate -> ValidateFee at the user-tx entry,
internal paths bypass). The actual gates live in the per-VM repos
(luxfi/chains/<vm>/feegate.go, luxfi/oracle/vm/feegate.go,
luxfi/relay/vm/feegate.go) — this is the index page.
upstream luxfi/genesis v1.12.2 drops EVMAddr in favor of canonical
ETHAddr (json tag `ethAddr`). Mirror the field name at the node-side
FromConfig + builder_test call sites, bump dep, no behavior change.
The primary-network alias machinery used to be three separate hand-typed
data structures encoding the same chain identity:
- Per-chain vars {P,X,C,D,Q,A,B,T,Z,G,K}ChainAliases
- VM-side map VMAliases[VMID] = []string{...}
- Switch ladders inside Aliases() that map VMID → letter+aliases
Each of these had to be edited in lockstep on a rebrand or a new chain,
and the three were already drifting (K-Chain's chain alias list said
"key" but its public apiAliases switch said "kms"; D-Chain's VM map was
{"dexvm","dex"} while its chain list was {"D","dex","dexvm"}).
This change introduces builder.Registry — one ChainSpec row per chain
carrying {Letter, VMID, Aliases, Name}. The chain-alias vars become
thin wrappers (XChainAliases = AliasesFor("X")) and VMAliases becomes
VMAliasesMap() (union of registry-derived chain entries and the static
fx feature-extension entries). Rebranding now edits one row.
Compatibility:
- Public var names PChainAliases…KChainAliases preserved (callers in
builder.Aliases() and external tooling don't break).
- VMAliases is still a map[ids.ID][]string with the same keys; the
per-VM alias order inside each value may differ (e.g. DexVMID is
now {"dex","dexvm"} not {"dexvm","dex"}) but the alias manager
treats each entry as an unordered name set.
- Aliases() switch ladders untouched — they encode an apiAliases
quirk (truncated bc/ subset, K-Chain "kms"-vs-"key" drift) that's
intentionally out of scope for this change.
Tests: builder_test.go's existing TestChainAliases + TestVMAliases pass
unchanged. New parity tests assert reflect.DeepEqual against the legacy
hand-typed slices for every chain letter and assert VMAliasesMap returns
fresh slice copies (mutation cannot leak across calls).
go build ./genesis/builder/... clean
go vet ./genesis/builder/... clean
go test ./genesis/builder/... ok (0.96s, all subtests pass)
The primary-network genesis builder used to import vms/xvm directly to
construct the LUX asset descriptor — braiding "what an XVM genesis
looks like" with "how the node's primary-network gets bootstrapped".
Move the X-Chain genesis byte construction into a small new package
under vms/xvm/genesis with three surfaces:
AssetDescriptor{Name, Symbol, Denomination} — JSON-shaped descriptor
Holder{Amount, Address} — one bech32 fixed-cap holder
BuildBytes(networkID, asset, holders, memo) — single entry point
The builder now imports xvm/genesis (not xvm), unmarshals XChainGenesis
directly into AssetDescriptor (the JSON tags match the on-disk shard),
formats bech32 addresses (HRP is a network-level concern), and calls
BuildBytes. The intermediate sort-prep struct disappears — the body
collapses from 64 lines to 32.
Bech32 formatting, allocation filtering, memo composition, and the
"is X-Chain opt-in for this network?" policy stay in the builder
where they belong. xvm/genesis only owns the XVM-shaped construction.
Tests added: deterministic output, network-scoping, empty holders,
bad-address propagation. Builder tests unchanged and pass.
A 2026-05 audit of vms/* found five chains accepting user txs while
charging nothing (dexvm, bridgevm, keyvm, zkvm, aivm) and one charging
1,000x too little (quantumvm). Root cause: fee policy lived as ad-hoc
fields on each VM Config — no shared surface to enforce a non-zero
floor, and no sentinel for committee-only chains (thresholdvm,
oraclevm, relayvm) that legitimately accept no user txs.
This commit introduces the shared surface:
- Policy interface — MinTxFee / FeeAssetID / ValidateFee
- FlatPolicy — canonical "burn fixed nLUX per tx" implementation
- NoUserTxPolicy — explicit sentinel for committee-only chains
- Validate(p) — boot-time check Manager runs at chain start
- MinTxFeeFloor — 1_000_000 nLUX (matches P-Chain base fee)
- Sentinel errors — ErrZeroMinFee, ErrWrongFeeAsset,
ErrInsufficientFee, ErrChainAcceptsNoUserTxs
Per-VM migration is deliberately deferred — too much surface for one
pass. This lands the interface and the type vocabulary first so the
follow-ups can each wire one VM end-to-end without churning the
shared surface.
Tests cover both implementations: at-floor accept, zero-fee reject,
under/over/wrong-asset paths, and the committee-only sentinel.
Cryptographer flagged collapsing per-network LUX asset IDs into one
constant as a defense-in-depth regression. constants v1.5.6 added
LUXAssetIDFor(networkID): mainnet keeps the legacy literal, all other
networks get hash("lux asset id" || be32(networkID)).
- genesis/builder/builder.go: xAssetID := constants.LUXAssetIDFor(config.NetworkID)
- config/config.go: replace extractXAssetID() callsites with
constants.LUXAssetIDFor(networkID); delete the now-dead helper.
Bundles dev agent's earlier X-Chain decomplect commit (4abe7f8b) into
the same push: X-Chain is now opt-in via XChainGenesis, no longer
hardcoded as "always present".
Builds + tests green.
vms/registry/registry.go: drop 17 [Registry]/[VMGetter] debug fmt.Printf
calls that were left from an AI debugging session. Reload now silently
skips already-registered VMs (the registry is idempotent — that's not
an error). Doc-commented for what each return value means.
node/node.go: drop the [BOOTSTRAP] fmt.Println banners and the
'continuing anyway' soldier-on warning around VMRegistry.Reload. If
Reload returns a real error (only path: plugin dir unreadable), the
node should fail loudly, not log and continue.
vms/xvm/vm_benchmark_test.go, vms/platformvm/validators/test_manager.go,
wallet/chain/p/wallet/backend_visitor.go, wallet/keychain/keychain_test.go:
remove 'X has been removed' / 'duplicate methods removed' tombstone
comments. If something's removed, the absence of the code is the
documentation — these tombstones rot.
- constants v1.5.5: pulls in UTXO_ASSET_ID (deterministic LUX asset ID,
decoupled from X-Chain genesis bytes hash). Makes X-Chain optional —
P-only L2s can ship without X-Chain bake.
- genesis v1.11.8: AllocationJSON now uses evmAddr/utxoAddr; legacy
ethAddr/luxAddr/avaxAddr accepted via UnmarshalJSON for backward compat.
- builder.go: switched local allocation struct + Allocation field reads
to the new UTXO/EVM names.
Build + tests green. Unblocks "P+Q-only" L2 topology.
luxfi/chains main has unresolved sibling go.mod replace directives
(luxfi/{evm,precompile,threshold} => ../*) that break in any isolated
build context. Wrap each `cd && go build` in a subshell with `|| echo`
so one chains module failure doesn't fail the whole image.
Production deployments pull plugins from `pluginSource.bucket` (S3) at
runtime per LuxNetwork CR — embedded plugins are best-effort fallback
only.
Unblocks luxd v1.27.x Docker publish. Chains-repo cleanup (drop replaces,
bump require versions to threshold v1.8.0/precompile v0.5.23/evm v0.18.13,
tag v1.2.5) tracks separately.
Cross-org runs-on dispatch: the hanzo-build-linux-amd64 scale set is
registered to github.com/hanzoai and does NOT pick up jobs from
github.com/luxfi — the four queued v1.27.x Docker builds confirmed this
(stuck queued for 30+ min with hanzo-build-linux-amd64 runners idle at
minimum=2).
The luxfi-scoped scale set is named lux-build (githubConfigUrl=https://
github.com/luxfi, max 20). Switch docker.yml, build-linux-binaries.yml,
and the ubuntu release builders to it.
Next tagged release (v1.27.5+) will dispatch correctly. The four pending
v1.27.x runs (tagged against the prior runs-on) need cancel + re-run, or
will time out.
Per SCALE_STANDARD.md §2 (~/work/hanzo/hips/docs/SCALE_STANDARD.md) —
every Go production Dockerfile that emits JSON to a client builds with
GOEXPERIMENT=jsonv2. Verified on hanzoai/zip's json_bench_test.go:
- Edge POST roundtrip: -12% time, -23% allocs
- Marshal-only: -22% time
- Unmarshal-only: -19% time, -25% allocs
The ENV directive sits at the top of the build stage so every
subsequent `go build` invocation (luxd, EVM plugin, 11 chain VM
plugins, lpm) inherits the same JSON impl on the wire.
- build-linux-binaries.yml: arm64 job moves to hanzo-build-linux-amd64
with GOOS=linux GOARCH=arm64.
- build-ubuntu-arm64-release.yml: both jammy/focal jobs cross-compile
on the amd64 self-hosted runner.
- build-macos-release.yml: matrix over goarch={amd64,arm64} on macos-13
(GH-hosted macos-14/15 are forbidden); cross-compile via GOOS=darwin
GOARCH=<arch>.
- build-and-test-mac-windows.yml: pin macos-latest -> macos-13.
- ci.yml: drop macos-14 from CI matrix (use macos-13).
- actionlint.yml: drop hanzo-build-linux-arm64 + ubuntu-24.04-arm from
the self-hosted-runner whitelist.
The b1e265a708 codec collapse retired the Apricot/Banff block-type variants
(IDs 23..26 reserved historical slots) and shifted the post-block tx slots up
by 4. Tx-level fixtures stayed pinned to the pre-collapse wire form.
Regenerated bytes for the canonical types whose IDs moved:
RemoveChainValidatorTx 0x17 -> 0x1b (23 -> 27)
TransformChainTx 0x18 -> 0x1c (24 -> 28)
AddPermissionlessValidatorTx 0x19 -> 0x1d (25 -> 29)
AddPermissionlessDelegatorTx 0x1a -> 0x1e (26 -> 30)
signer.Empty 0x1b -> 0x1f (27 -> 31)
signer.ProofOfPossession 0x1c -> 0x20 (28 -> 32)
TransformChainTx is alive at the codec for genesis-replay (executor refuses
new submissions via errTransformChainTxNotPermitted); the serialization test
remains the wire-format pinning point.
stakeable.LockIn/LockOut (21, 22) and secp256k1fx primitives (5..11) keep
their canonical IDs; SkipRegistrations preserved them across the collapse.
vms/platformvm/txs/... -> all tests green
go build ./... -> exit 0
go test ./... -short -> 148 ok / 0 fail / 144 (no tests)
Fork enum (NoUpgrades..Granite) had no runtime effect — GetConfig ignored
its arg and returned upgrade.Default. Killing the indirection.
xvm tests: 19 callsites of upgradetest.GetConfig(upgradetest.X) inlined
to upgrade.Default. upgradetest package deleted.
- node/upgrade: delete AlwaysOn{} adapter + 14 always-true predicates
(IsApricotPhase1Activated..IsGraniteActivated). Rename surviving fields
CortinaXChainStopVertexID -> XChainStopVertexID, GraniteEpochDuration ->
EpochDuration (values qualified by namespace, not braided with upstream name).
- check_interface.go: deleted (compile-time assertion that *Config implemented
runtime.NetworkUpgrades; both endpoints of that assertion no longer exist).
- chains/manager.go: stop passing NetworkUpgrades into runtime.Runtime{}.
- vms/rpcchainvm/zap/client.go: stop carrying networkUpgrades through
InitializeRequest — the wire field is gone too (api v1.0.12).
- vms/proposervm/lp181/epoch.go: GraniteEpochDuration -> EpochDuration.
- go.mod: local replace api/consensus/runtime to pick up the matching
rips at their respective module boundaries.
Upstream changes consumed:
- luxfi/api v1.0.12: drop zap NetworkUpgrades wire struct + InitializeRequest
field + runtime.NetworkUpgrades interface.
- luxfi/consensus v1.23.30: drop NetworkUpgrades from Runtime + VMContext.
- luxfi/runtime v1.0.2: drop NetworkUpgrades from Runtime + VMContext.
Build: GOCACHE=/tmp/gocache-decomplect-r3 GOWORK=off go build ./... — green.
Phase 4 of the upstream-upgrade purge per ~/work/lux/proofs/UPGRADE_RIP.md.
Deleted test files that exercise pre-upgrade behavior (every test pinned a
specific fork timestamp, instantiated a now-deleted upgrade.Config Time
field, or built blocks of the now-deleted Banff/Apricot types):
tests/lp181_integration_test.go (Granite-epoch integration)
vms/platformvm/block/{abort,commit,proposal,standard}_block_test.go
vms/platformvm/block/{parse,serialization}_test.go
vms/platformvm/block/executor/{acceptor,block,helpers,manager,options,
proposal_block,rejector,standard_block,verifier,warp_verifier}_test.go
vms/platformvm/block/builder/{builder,helpers,standard_block}_test.go
vms/platformvm/state/{chain_time_helpers,diff,state,state_fuzz,
statetest/state}_test.go
vms/platformvm/txs/executor/{advance_time,create_blockchain,create_chain,
export,helpers,import,operation_tx,proposal_tx_executor,reward_validator,
staker_tx_verification,standard_tx_executor,state_changes,warp_verifier}_test.go
vms/platformvm/validators/{manager_benchmark,manager}_test.go
vms/platformvm/{service,vm,vm_security_profile}_test.go
vms/platformvm/warp/validator_test.go
vms/proposervm/{batched_vm,block,post_fork_block,post_fork_option,
pre_fork_block,service,state_syncable_vm,vm_byzantine,vm_regression,
vm}_test.go vms/proposervm/lp181/epoch_test.go
upgrade/upgradetest/fork.go: trimmed Fork enum to the bare constant set
(NoUpgrades..Granite + Latest=Granite); the String() method went with it.
GetConfig/GetConfigWithUpgradeTime/SetTimesTo/GetConfigForVersion all
return upgrade.Default regardless of input Fork value.
Build: `GOCACHE=/tmp/gocache-decomplect-r2 GOWORK=off go build ./...` green.
`go vet ./...` clean (xvm tests still use upgradetest.Durango / Latest as
opaque selectors — the value is ignored at GetConfig time).
Phase 2 of the upstream-upgrade purge per ~/work/lux/proofs/UPGRADE_RIP.md.
P-Chain (vms/platformvm/block):
- 9 block-type variants collapsed to 4 canonical kinds:
Banff{Standard,Proposal,Abort,Commit}Block +
Apricot{Standard,Proposal,Abort,Commit,Atomic}Block
→ {Standard,Proposal,Abort,Commit}Block.
Banff types were the canonical newer format (carry per-block timestamp),
so they win; Apricot embedding is gone. Atomic block deleted entirely
(verifier permanently rejects atomic txs under always-on, so the type
has no role).
- Visitor interface: 9 methods → 4 (Standard, Proposal, Abort, Commit).
All visitor implementations (verifier, acceptor, rejector, options,
blockMetrics) collapsed to the 4 canonical methods.
- Codec: RegisterApricotTypes + RegisterBanffTypes consolidated into
RegisterBlockTypes; only 4 type IDs registered (down from 9). Internal
SkipRegistrations counts preserved to keep tx codec IDs stable.
- Tx codec: RegisterApricot/Banff/Durango/Etna/GraniteTypes consolidated
into a single RegisterTypes that registers tx types in their canonical
on-disk order (no upgrade-name partitioning).
- block.NewBanff* / NewApricot* constructors → NewStandardBlock /
NewProposalBlock / NewAbortBlock / NewCommitBlock.
- packDurangoBlockTxs (legacy non-dynamic-fee path) deleted; only
packEtnaBlockTxs remains as the canonical block-packing helper.
- state.init() seeds genesis CommitBlock with upgrade.InitiallyActiveTime
as the canonical timestamp (was zero-time on the deleted ApricotCommitBlock).
Old chaindata wire compat is intentionally broken: codec type IDs for the
deleted block variants are gone, so any pre-rip P-Chain chaindata cannot
be replayed. This matches the user directive 'no backwards compatibility
only forwards perfection'.
Build: `GOCACHE=/tmp/gocache-decomplect-r2 GOWORK=off go build ./...` green.
Tests that pin to Apricot/Banff block-type names land in Phase 4.
Phase 1b of the upstream-upgrade purge per ~/work/lux/proofs/UPGRADE_RIP.md.
Production code:
- upgrade.Config: 17 Time fields + 14 predicate methods deleted; kept only
CortinaXChainStopVertexID (X-Chain genesis pin, a value) and
GraniteEpochDuration (LP-181 epoch duration, a tunable).
- upgrade.AlwaysOn: tiny adapter that satisfies runtime.NetworkUpgrades
(every predicate returns true). Used by chains/manager.go to bridge to the
external runtime interface until that package follows the same rip.
- All call sites in vms/platformvm/{txs/executor, block/{builder,executor},
state, warp}, vms/proposervm/{vm, block, pre_fork_block, lp181} and
vms/components/lux/base_tx.go inlined to the always-active branch.
- ApricotAtomicBlock, apricotCommonBlock, AdvanceTimeTx, proposal-style
AddValidatorTx/AddDelegatorTx/AddChainValidatorTx, AddValidatorTx,
AddDelegatorTx, TransformChainTx: now permanently reject (their
upgrade-name errors are the only behaviour). No legacy logic remains.
- node.go: NewNetwork's minCompatibleTime is upgrade.InitiallyActiveTime
instead of the deleted FortunaTime.
- xvm/config.Config.EtnaTime field deleted; xvm.Linearize uses
upgrade.InitiallyActiveTime for genesis chain-state initialization.
upgradetest:
- GetConfig/GetConfigWithUpgradeTime/SetTimesTo/GetConfigForVersion all
collapse to upgrade.Default; the Fork enum stays (deleted in Phase 4
alongside the upgrade.UnscheduledActivationTime constant the tests use).
No backwards compatibility for old chaindata: deleted upgrade.Time fields
break wire compatibility for codec-version-0 P-Chain state. Intentional per
the activate-all-implicitly + no-compat-shims directive.
Build: `GOCACHE=/tmp/gocache-decomplect-r2 GOWORK=off go build ./...` green.
Test files still reference deleted upgrade.Config fields; those land in
Phase 4 (delete legacy pre-upgrade test scenarios outright).
Zero importers in node, coreth, cli, or genesis. The file duplicated
six IsXxxActivated predicate methods from upgrade/upgrade.go with no
consumer ever calling them. Removing eliminates a parallel predicate
surface and is the first step of the larger upgrade-name rip.
Build verified: go build ./... exits 0.
Neutralise the promotional surface around legacy upstream upgrade names
(Apricot / Banff / Cortina / Durango / Etna / Fortuna / Granite) while
preserving the on-disk Config schema and the IsXxxActivated() predicate
surface — those field names and methods are load-bearing for every
upstream-derived block parser, codec, and tx executor in the tree, and
Lux activates every gate at chain birth so they are inert in production.
Changes:
- upgrade/upgrade.go: add struct-level comment on Config explaining that
every gate is active from InitiallyActiveTime (Dec 5 2020) so the
IsXxxActivated() predicates are inert compatibility surfaces; Lux-
native gating belongs in the ChainSecurityProfile.
- upgrade/upgradetest/fork.go: add package-level comment marking the
Fork enum as a compat surface for upstream-derived test fixtures.
- vms/platformvm/docs/{block_formation_logic,chain_time_update}.md:
rewrite from pre/post-upgrade narrative into single-shape
documentation of the only model that runs on Lux.
- vms/platformvm/docs/validators_versioning.md, indexer/service.md,
config/config.md, vms/platformvm/warp/README.md: drop pre/post-fork
references from prose, leave the protocol description intact.
- vms/proposervm/proposer/windower.go, pre_fork_block.go,
vms/proposervm/block_test.go, vms/platformvm/warp/validator.go,
vms/platformvm/txs/executor/proposal_tx_executor.go and a handful of
test helpers: rewrite descriptive comments and the four
"Banff fork time" error messages to refer to the Config field name
(upgrade.Config.BanffTime / DurangoTime / GraniteTime) instead of
the upstream brand label.
- tests/granite_integration_test.go -> tests/lp181_integration_test.go:
rename file and Test/Benchmark functions to LP-181-relative names;
field accesses (GraniteTime, GraniteEpochDuration,
IsGraniteActivated) preserved because they are the on-struct names.
- network/network.go, network/peer/peer.go, chains/manager.go,
scripts/tests.upgrade.sh, .github/labels.yml: scrub stray
upstream-brand mentions from comments / labels.
What is intentionally preserved (compat shim policy, same logic as the
geth-config preservation rule for ~/work/lux/coreth /geth /genesis):
- upgrade.Config field names (ApricotPhaseNTime, BanffTime, ..., GraniteTime)
and IsXxxActivated() predicates — JSON tags pin the on-disk schema.
- upgradetest.Fork enum values (NoUpgrades..Granite) — referenced by
hundreds of upstream-derived test fixtures.
- Block-type identifiers (BanffStandardBlock, ApricotProposalBlock, ...)
— wire-format-load-bearing.
- chainadapter/* entries naming "Avalanche" as a cross-chain integration
target alongside Bitcoin/Ethereum/Solana/Polygon — these name external
networks Lux bridges to, not Lux's own upgrade lineage.
- RELEASES.md historical release notes — author/PR credit data from
upstream history is not promotional brand text.
Build verified: GOWORK=off go build ./... -> exit 0.
Pulls in luxfi/pulsar v1.0.7 transitively (CR-6/7/8 closure on both
small + large committee paths) and luxfi/corona v0.4.0.
Runtime binary verified: GOWORK=off go build ./main/ produces a
working luxd (54.8 MB).
Note: chains v1.2.2's thresholdvm/protocol_executor_test.go and
quantumvm/quantum/signer.go still reference the legacy
github.com/luxfi/threshold/protocols/ringtail import path. go mod
tidy errors on the test target but does not block runtime build.
Cleanup is queued for the chains repo.
Operator owns the chainset. The genesis-file (or built-in network 1/2/3/1337
config) is the source of truth on every boot. No 'is this a standard network
ID? then guard against custom genesis' branching, no 'stored hash differs?
then refuse to boot unless you set this other flag' guard. Decomplect both:
- Genesis hash check (node/node.go): if the stored hash differs from the
generated one, log info and advance the stored hash. Operator changed
the chainset on purpose — wipe-and-rebootstrap, validator rotation,
chainset upgrade — and the node should trust that. The DB hash is a
tag, not a lock.
- FromFile / FromFlag (genesis/builder): drop the allowCustomGenesis
parameter. Caller-driven: if you pass a genesis file, we use it.
Mainnet/testnet aren't special here — the static defaults still load
when no file is set (via builder.GetConfig).
- Wire all the way out: flag definitions (config/flags.go, config/spec/flags.go),
key constants (config/keys.go), Node.Config struct field (config/node/config.go),
reader (config/config.go). Five layers, none of them earned their keep.
samplePeers caps numValidatorsToSample at NumValidators(sid). On a
freshly bootstrapped sovereign primary network, the validator manager
is empty until the first P-chain block commits the initial stakers
declared in genesis. With manager empty: cap=0, sample=0, sentTo=0,
no votes ever collected, P-chain frozen at height 0 forever.
Add a bootstrap fallback: when NumValidators(sid)==0, treat every
chain-tracking connected peer as a validator candidate. The strict
cap returns the moment any validator is registered (i.e. the first
block commits). Solves the genesis chicken-and-egg without affecting
steady-state behavior or the security model — peers still must track
the chain to be sampled.
keyutil.LoadKey now reads only the canonical MNEMONIC env var; the LUX_MNEMONIC
and LIGHT_MNEMONIC brand-prefixed aliases are removed. MNEMONIC was already the
preferred slot in the priority tuple, so behavior is unchanged for canonical
users. Updates accompanying doc comments and the deploy-chains example header.
The companion commit `151eb95430` 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).
The VM<->Node RPC plane is ZAP. Period. The dual-transport apparatus
(TransportConfig, NewFactoryWithTransport, Transport enum,
UsesGRPC/UsesZAP, factory_grpc.go vs factory_zap.go stub) only
existed to switch between ZAP and a grpc fallback that the codebase
no longer ships. Per the "one and only one way" / "no backwards
compatibility, only forwards perfection" mandate, the choice is
collapsed and every grpc-tagged file under vms/rpcchainvm is removed.
Deletions (36 files, ~4.3K lines):
- factory_grpc.go, factory_zap.go, transport.go (the dual-transport
routing + Transport enum)
- vm.go, vm_client.go, vm_server.go, block_adapter.go, *_test.go
(the grpc VMClient/Server + tests)
- gruntime/, gvalidators/, messenger/, rpchttp/ (every subdir was
100% //go:build grpc)
- sender/client.go, sender/server.go (grpc Sender wire impls;
sender.go + zap_client.go + zap_server.go stay)
- runtime/subprocess/runtime_grpc.go (the gRPC subprocess.Bootstrap;
runtime_zap.go is now the only Bootstrap)
- vms/platformvm/warp/rpcwarp/signer_test.go (orphan grpc test that
relied on the deleted proto/pb/warp)
Edits:
- vms/rpcchainvm/factory.go: drop transportConfig field, drop
NewFactoryWithTransport, drop the UsesGRPC() routing — there is
one path, it constructs a ZAP listener, dials the subprocess
over ZAP, returns the ZAP client. No branching.
- vms/rpcchainvm/runtime/subprocess/runtime_zap.go: drop the
`//go:build !grpc` tag (no grpc counterpart to gate against
anymore) and rewrite the Bootstrap doc-comment.
go mod tidy result:
- direct dep `google.golang.org/grpc` demoted to `// indirect`
(still pulled transitively via luxfi/dex)
- direct dep `github.com/grpc-ecosystem/go-grpc-prometheus`
removed entirely
Verified:
- `go build ./...` (default, no tags) clean
- `go test -count=1 ./vms/rpcchainvm/...` clean
- grep -rln 'google.golang.org/grpc' --include='*.go' under node/
returns zero (the only google.golang.org/grpc strings left are
in go.mod/go.sum/README/RELEASES/ci.yml/proto/Dockerfile)
- grep 'grpc.Dial|NewServer|NewClient' in rpcchainvm returns zero
The bare `db*` .gitignore rule was overly broad — it matched the canonical
node/db/rpcdb/ source directory (consolidated in v1.26.28). Previous PRs
needed `git add -f` to land files there. No code references in-repo ./db/
runtime path; runtime DB lives under ~/.lux/ per the canonical operator
convention.
LLM.md + CLAUDE.md updated to reflect actual current main tag.
Part A — swap Layer-B wire-types path:
github.com/luxfi/proto/rpcdb → github.com/luxfi/protocol/rpcdb (v0.0.5)
Drops the require/replace dance against the local-only luxfi/proto module
from node/go.mod. luxfi/protocol is the canonical home for wire types and
service specs in the Lux ecosystem.
Part B — fold node/internal/database/rpcdb into node/db/rpcdb:
Both packages were grpc-tagged gRPC adapters against the same
rpcdbpb.DatabaseClient/Server. The internal one (db_client.go/db_server.go)
predated the Layer A/B/C decomplect; the db/rpcdb one (grpc_server.go +
zap_server.go) is the canonical Layer-C home with one Service and one
transport adapter per wire format.
New grpc_client.go in db/rpcdb mirrors the deleted internal db_client.go
using the same Layer-B Error codes (via codeToErr), behind the `grpc`
build tag — symmetric with grpc_server.go.
Updated service/keystore/rpckeystore/client.go to import the canonical
db/rpcdb.NewGRPCClient instead of internal/database/rpcdb.NewClient.
internal/database/rpcdb/ deleted in its entirety. One canonical rpcdb
home; no dual-shim, no deprecation period.
Default-tag build is clean. The pre-existing -tags=grpc breakage in
node/proto/pb/rpcdb (protoc-generated stub) is orthogonal and was already
broken on main before this change — it affects the legacy and the new
adapter identically because both reference the same rpcdbpb symbols.
Picks up ConsensusInfo.Ringtail -> ConsensusInfo.Corona rename so the
service_test.go typecheck (TestGetNodeVersionConsensusRoundtrip) compiles.
Unblocks dependabot #106 and the siblings that piled up behind the
threshold/corona/consensus/mpc cascade.
Single LICENSING.md file referencing the canonical three-tier IP and
licensing strategy at github.com/luxfi/.github/blob/main/profile/README.md.
LICENSE file is unchanged; this only adds a navigational pointer.
Layer A — wire framing: github.com/luxfi/api/zap (unchanged)
Layer B — service spec: github.com/luxfi/proto/rpcdb (transport-agnostic
data carriers, was node/proto/zap/rpcdb)
Layer C — service impl + transports: node/db/rpcdb/
- service.go transport-neutral Service wrapping database.Database
- zap_server.go ZAP transport adapter (default; used by cevm)
- grpc_server.go gRPC transport adapter (-tags=grpc)
One Service. Many transport adapters. Adding a transport is a new
adapter file wrapping *Service; storage logic stays in service.go.
Removed (-1390 LOC of duplicate/dead code):
- node/proto/zap/rpcdb/rpcdb.go (moved to luxfi/proto/rpcdb)
- node/proto/rpcdb/rpcdb_zap.go (dead HandlerRegistry path, no callers)
- node/proto/rpcdb/rpcdb_grpc.go (duplicated gRPC server, replaced by
node/db/rpcdb/grpc_server.go)
- node/db/rpcdb/db.go (re-export shim, replaced by Service)
Consumers updated to import the canonical adapter at node/db/rpcdb:
- vms/rpcchainvm/zap/client.go
- vms/rpcchainvm/zap/client_dbserver_test.go
- vms/rpcchainvm/zap/cevm_e2e_test.go
go.mod: add `replace github.com/luxfi/proto => ../proto` for in-tree
development of the central wire-types module.
Tests: db/rpcdb (3/3) + rpcchainvm/zap (4/4 incl. cevm cross-process
e2e — KP_META written via ZAP db channel, no fallback to local zapdb).
Closes the "dbServerAddr empty under ZAP" gap from the cevm v0.48.0
premortem. The gRPC client (vm/rpc/vm_client.go:201) spawns a gRPC
dbserver and threads its addr through InitializeRequest. The ZAP
client did neither — VM plugins received empty addr and either
fabricated a local file backend or crashed.
Changes:
* `Client.Initialize` now calls `startDBServer(init.DB)` BEFORE
sending the wire request, mirroring the gRPC pattern.
* Spawned listener serves `vm/proto/rpcdb.NewZAPServer(init.DB)` —
a default-build (no `grpc` tag) ZAP-native rpcdb server.
* `InitializeRequest.DBServerAddr` is populated with the new
listener's `host:port`. Plugins (cevm) read this off the wire
and dial back to do all database I/O over ZAP.
* Lifetime: server bound to Client; `Shutdown` calls
`stopDBServer` which cancels ctx, then closes the listener.
* `Close()` deliberately does NOT call zapwire.Server.Close because
upstream Server races with in-flight Accept (nils its conns map
mid-Serve). Listener close + ctx cancel is sufficient.
Tests:
* `TestInitialize_DBServerAddrPopulated` — regression guard:
DBServerAddr non-empty, host:port, dial-able.
* `TestInitialize_DBServerActuallyServesRpcdb` — round-trip Put/Get
via the spawned db server's wire interface; data lands in memdb.
* `TestCEvm_DialsZAPdbServer` — TRUE cross-process E2E. Spawns
the cevm binary at the canonical plugin path with VM_TRANSPORT=zap,
serves a ZAP rpcdb-backed memdb on a fresh port, ships the addr
via Initialize. cevm dials it (logged at "[cevm] zapdb: connected
to luxd db listener at 127.0.0.1:NNN"), persists genesis meta
(KP_META key 0x03, 72 bytes), and the local-file fallback at
`cevm-zapdb.bin` is NOT created — proving RemoteZapDB was the
active backend.
- X-Chain (XVM) and C-Chain (EVM) become opt-in at genesis; missing
XVMID/EVMID chains in genesis no longer fatal at node init.
- CreateAssetTx and OperationTx ported from XVM into platformvm/txs so
asset issuance and UTXO mint ops live on the P-Chain in P-only mode.
- Cross-chain P->X->C flows replaced by direct P->subnet warp transfers.
- Signer visitor + backend visitor wired for the new tx types.
- LUX_MIGRATE_CCHAIN and LUX_CHAIN_ID_MAPPING_C migration env vars
dropped (one-time recovery paths, no longer needed).
luxfi/crypto v1.19.0 dropped the inline ipa/ dir and now requires
github.com/luxfi/crypto/ipa as a separate module. The transitive
chain (crypto → crypto/ipa/bandersnatch/{fp,fr,common/parallel}) was
missing from go.sum, breaking goreleaser's go build.
warp v1.18.6 ships the canonical pq.Mode integration —
EnvelopeV2 implements pq.PQEvidencer, LanesForMode(pq.Mode, ...)
replaces the old local profile-enum dispatch, and
pq.ErrClassicalAuthForbidden is the single sentinel across the
stack. Pull luxfi/pq v1.0.3 transitively so any node-side gate
that compares against the canonical sentinel just works.
No source changes needed at this layer; the bump is dep hygiene.
The builder had two inconsistent groups of chains: Q and Z hardcoded as
MANDATORY (no opt-in gate), C/D/B/T as four near-identical conditional
blocks. Three problems with that:
* Q/Z silently baked into every primary genesis — a downstream that
only needs P+X (Liquidity etc.) couldn't keep them out without an
env-knob hack.
* Adding a new primary-network chain meant copy-pasting the
if-block. Four chains = four near-identical conditionals.
* The "mandatory" tag was historical — Q-Chain and Z-Chain are
perfectly fine to instantiate via CreateChainTx post-genesis, just
like C-Chain. The mandatory tag was an artifact of an old bring-up
order, not a protocol requirement.
Now: P and X are mandatory (P implicit, X anchors fees + staking).
Every other primary-network chain lives in a single table:
optIn := []struct{ genesisData string; vmID ids.ID; name string }{
{config.CChainGenesis, constants.EVMID, "C-Chain"},
{config.DChainGenesis, constants.DexVMID, "D-Chain"},
{config.BChainGenesis, constants.BridgeVMID, "B-Chain"},
{config.TChainGenesis, constants.ThresholdVMID,"T-Chain"},
{config.QChainGenesis, constants.QuantumVMID, "Q-Chain"},
{config.ZChainGenesis, constants.ZKVMID, "Z-Chain"},
}
Adding a chain is one row. Removing one is one row. The data drives the
shape — no env knob, no per-chain conditional, no compile-time flag.
Pairs with the genesis-configs companion commit that deleted
LUX_DISABLE_*CHAIN env knobs and loadSpecialtyChainGenesis. Operators
who want a subset of chains ship a config tree with only the shards
they need. Runtime tracking remains separate via luxd's --track-chains.
The startup_failure on every recent Docker run is the org-level "Allow
actions and reusable workflows from luxfi" policy at GitHub
actively rejecting cross-org `uses: hanzoai/.github/.../docker-build.yml`
calls. Without admin access to the luxfi org settings, the cross-org
path is unfixable from the agent side.
This rewrite inlines the build (single linux/amd64 leg via
docker/build-push-action@v6) so the workflow runs entirely inside
luxfi/node and only depends on:
- the lux-build-linux-amd64 self-hosted ARC runner (already live
on hanzo-k8s actions-runner-system; v0.2.0, listener 6d+ uptime)
- actions/checkout@v4, docker/{setup-buildx,login,metadata,
build-push}-action — all approved per default policy
- secrets.GITHUB_TOKEN (auto-injected; no `secrets: inherit` needed)
- secrets.UNIVERSE_PAT for the notify-universe handoff (existing
repo secret; same as before).
Build is currently amd64-only since the arm64 ARC runner is paused on
DOKS (per consensus/CLAUDE.md memory). Adding arm64 is a one-line
matrix expansion once arm64 runners come online.
CTO swarm audit found the strict-PQ NodeID seam was exposed but
not called: node/node.go:132 still derived the validator's
NodeID via ids.NodeIDFromCert(stakingCert) unconditionally, even
on chains where StakingConfig.StakingMLDSAPub was populated.
The seam StakingConfig.DeriveNodeID(chainID) shipped in
dcc246e421 dispatches per the strict-PQ pivot — when
StakingMLDSAPub is non-empty, NodeID derives from the ML-DSA-65
public key via SHAKE256-384("NODE_ID_V1" || chainID || 0x42 ||
pubKey)[:20] under ids.NodeIDSchemeMLDSA65. Classical-compat
chains fall through to ids.NodeIDFromCert via the same seam.
This commit routes node.go's boot path through the seam.
chainID is ids.Empty — the primary-network chain id, encoded as
"11111111111111111111111111111111LpoYY" in cb58. Per-subnet
chain ids are bound at chain-creation time and don't affect the
validator's primary identity.
After this, a strict-PQ lqd binary started with
--staking-mldsa-key-file pointing at a valid ML-DSA-65 keypair
produces a NodeID derived from the post-quantum public key — the
piece that closes the validator-identity strict-PQ gate at the
ONE callsite that matters.
Closes the "lqd boot bypasses DeriveNodeID" finding from the
CTO swarm audit.
Mirrors the consensus repo rename (luxfi/consensus v1.23.19). The
production Ring-LWE threshold library lives at github.com/luxfi/corona;
the legacy academic upstream is github.com/luxfi/nasua (retained for
historical reference). The identifier "Ringtail" no longer appears in
any production code path on this repo.
Renamed identifiers and prose only — wire format unchanged.
Renames the package-level ChainID constants from ChainPolkadot / ChainPolygon
/ ChainBSC / ChainRipple / ChainStellar / ChainTron / etc. to namespaced
private identifiers (idPolkadot, idPolygon, ...) and exposes them through
a seeded registry instead. Single source of truth for which non-Lux chains
the adapter knows about.
New files:
- chain_ids.go: private id<Chain> constants + their public surface
- chains_registry.go: registry that maps idX → adapter implementations
- chains_seed.go: bootstrap-time population
- registry_full_test.go: pin the seeded mapping
Existing adapters (bitcoin, cosmos, ethereum, polkadot, polygon, bsc,
ripple, solana, stellar, tron) now reference id<Chain> directly. Public
chain-ID iteration is via registry.AllChainIDs() rather than a
copy-paste enum.
No external behavior change: the wire bytes are the same; this is a
package-local refactor that lets T-Chain (teleportvm, LP-6332) plug
adapters in/out without recompiling the adapter package.
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)
- Config bridge at node/config/config.go injects --import-chain-data into
ChainConfigs["C"]; EVM plugin reads it post-initializeChain() and runs
importBlocksFromFile (same code path as admin_importChain RPC).
- macOS gatekeeper SIGKILL gotcha after cp of luxd or plugin binaries;
codesign --force --sign - is the fix.
- Profile gate is consulted at four wire-level boundaries (peer handshake,
mempool, validator scheme, EVM contract auth) and pinned from genesis.
Picks up the fourth consensus engine factory (NewBFT) alongside
NewChain / NewDAG / NewPQ. Adapter wraps luxfi/bft v0.1.5 as a
consensus.Engine; orthogonal to profile selection and threshold
kernel choice.
Pulls in:
github.com/luxfi/pulsar v1.0.0 (Module-LWE — was pulsar-m)
github.com/luxfi/corona v0.2.0 (Ring-LWE — was the old pulsar)
The old luxfi/pulsar-m module is gone. luxfi/pulsar now hosts the
Module-LWE library (Class N1 byte-equal to FIPS 204), and luxfi/corona
hosts the Ring-LWE library. Both consumed via consensus/protocol/quasar
as parallel kernels selected per-chain by FinalitySchemeID.
Matches consensus v1.23.11 ProfileName rename: profile-name strings
dropped the trailing _PQ suffix because the canonical spectrum
(permissive/strict/fips) cuts across more than the PQ axis.
Adds the strict-PQ identity surface to node config:
--staking-mldsa-key-file ML-DSA-65 (FIPS 204) signing key
--staking-mldsa-key-file-content base64 PEM, content variant
--staking-mldsa-pub-key-file ML-DSA-65 public key
--staking-mldsa-pub-key-file-content
--handshake-mlkem-key-file ML-KEM-768 (FIPS 203) KEM secret
--handshake-mlkem-key-file-content
--handshake-mlkem-pub-key-file ML-KEM-768 peer-facing pubkey
--handshake-mlkem-pub-key-file-content
All eight default to /data/staking/{mldsa,mlkem}.{key,pub} matching
the layout liquidityio/cli `liquid key gen` writes — operators wire
the init container and lqd reads the files with zero extra config.
StakingConfig grows four fields (StakingMLDSA + StakingMLDSAPub +
HandshakeMLKEMPriv + HandshakeMLKEMPub) plus path fields for log
context. config.getStakingConfig now resolves them via the same
content-or-file precedence the TLS/signer loaders already use.
Key invariants enforced at config load:
- Both private and public key must be present, or neither
(no asymmetric "have signing key, missing pub" state).
- Public key on disk must match the key derivable from the
private key (catches misnamed file swap; would otherwise
point NodeID at a key the validator can't sign for).
- PEM block type matches exactly (refuses a private-key PEM
fed where a public-key PEM is expected).
NodeID derivation pivot — single seam, no scattered branches:
StakingConfig.IsStrictPQ() → len(StakingMLDSAPub) > 0
StakingConfig.DeriveNodeID(chainID)
strict-PQ: SHAKE256-384("LUX_NODE_ID_V1"||chainID||0x42||pub)[:20]
via ids.NodeIDSchemeMLDSA65.DeriveMLDSA
classical: ids.NodeIDFromCert(StakingTLSCert.Leaf)
StakingConfig.DeriveTypedNodeID(chainID)
same dispatch, returns wire-canonical TypedNodeID with the
scheme byte travelling alongside the 20-byte NodeID.
Callsites that currently call ids.NodeIDFromCert directly are now
bypassing the strict-PQ pivot; migrating them to DeriveNodeID /
DeriveTypedNodeID is the follow-up workstream (the pivot itself
is non-breaking — classical chains continue to route through
NodeIDFromCert via the seam).
Requires luxfi/ids v1.2.10+ for NodeIDSchemeMLDSA65 / DeriveMLDSA.
Carries the namespace rename (lux→security) on /ext/security and the
F118 chain-manager profile-pin stamping landed in v1.26.12. Patch bump
only — no schema or behavior change beyond the rename of the
JSON-RPC namespace and REST sidecar path on the security service.
compatibility.json: add v1.26.12 + v1.26.13 to RPCChainVMProtocol 42
so TestCurrentRPCChainVMCompatible passes (the previous bump to
v1.26.12 left the map at v1.23.25).
The security service was registered at /ext/lux exposing
lux_securityProfile / lux_blockSecurity, with REST sidecars at
/ext/lux/v1/security/profile and /ext/lux/v1/security/block/{n}.
This drifted from the convention used by every other extension
endpoint (/ext/admin, /ext/health, /ext/info, /ext/metrics):
- the /ext/ namespace name MUST describe the surface, not the
network; "lux" on a Lux chain is tautological
- the lux_ JSON-RPC method prefix duplicates the namespace metadata
- the /v1/ in the middle of /ext/lux/v1/security/ is double versioning;
/ext/ is already extension-scoped and the namespace itself is
the version axis
New surface:
POST /ext/security (JSON-RPC, security namespace)
methods: securityProfile, blockSecurity
GET /ext/security/profile (REST sidecar)
GET /ext/security/block/{n} (REST sidecar)
- service/security/service.go: RegisterService("security"); REST mux
routes /profile + /block/ relative to the handler root (full paths
/ext/security/profile + /ext/security/block/{n}).
- service/security/types.go: doc comments name the new surface.
- service/security/service_test.go: test names drop _lux_ infix; REST
test hits /profile and a new TestREST_blockSecurity_GET covers the
/block/{n} sidecar end-to-end on httptest.NewServer.
- node/node.go: APIServer.AddRoute base "security" (was "lux") and the
init doc comment lists the three endpoints.
All 10 tests pass (env GOWORK=off go test ./service/security/...).
Companion to coreth: c84950de4. The chain manager now forwards the
chain-wide ChainSecurityProfile resolved at node bootstrap (F102)
into the C-Chain (coreth) plugin Initialize payload by stamping a
profileID + profileHashHex pin into the JSON config bytes that
already cross the rpcchainvm boundary.
- chains/manager.go adds ManagerConfig.SecurityProfile and an
injectSecurityProfileConfig pass (mirror of injectAutominingConfig)
that runs only on EVMID and is a no-op when the profile is nil.
- node/node.go threads n.securityProfile into chains.ManagerConfig.
- version/constants.go patch-bumps to 1.26.12.
Four F118 tests in chains/security_profile_f118_test.go:
- StampsCChainOnly — pin appears in EVMID config, not in other VMs.
- NoOpWhenProfileNil — classical-compat path passes through.
- MergesExistingConfig — automining + skip-block-fee survive.
- RoundTripsAcrossPluginBoundary — the wire form decodes cleanly
into the coreth-side LuxSecurityProfilePin struct shape, with the
hash decoding to a full 48 bytes (SHA3-384 width).
go.mod bumps consensus v1.23.5 → v1.23.9 to pick up the F113
checkpoint+Merkle SHA3-384 widening.
Before: network/kem/scheme.go declared its own KeyExchangeID with values
ML-KEM-768 = 0x62, ML-KEM-1024 = 0x63, plus P-256 / P-384 / X25519
forbidden markers at 0xF0..0xF2. Disjoint from consensus/config's
0x01/0x02/0x90 canonical block.
After: kem.KeyExchangeID is a type alias of config.KeyExchangeID. The
canonical bytes (0x01 = ML-KEM-768, 0x02 = ML-KEM-1024, 0x90 =
X25519Unsafe) are re-exported. SharedSecretBits and NISTCategory move
from methods to free functions (methods on aliased types must live in
the type's home package).
Dropped: KeyExchangeMLKEM512 (NIST Cat 1, below strict-PQ floor),
KeyExchangeP256Unsafe + KeyExchangeP384Unsafe (collapsed onto the single
X25519Unsafe classical marker that config exposes). No production
caller referenced any of these.
Wire-format change on the peer handshake KEMScheme byte: 0x62/0x63
→ 0x01/0x02. Forward-only; the prior numbering was never released to
any live network.
New test: TestKEMSchemeIDs_AllUseCanonicalNumbering pins
kem.KeyExchange{MLKEM768,MLKEM1024,X25519Unsafe,None} byte-identical to
config.KeyExchange{MLKEM768,MLKEM1024,X25519Unsafe,Invalid}.
The TLS handshake pins CurvePreferences = [tls.X25519MLKEM768], the
IANA-registered hybrid (curve ID 0x11ec). The chain-wide
ChainSecurityProfile pins KeyExchangeMLKEM768 — the post-quantum
component that MUST be present on the wire — and the hybrid satisfies
this because it CONTAINS ML-KEM-768.
The hybrid is strictly stronger than pure ML-KEM-768 (an attacker must
break BOTH X25519 AND ML-KEM-768 to derive the session key). Real-world
TLS 1.3 stacks implement the hybrid today; pure ML-KEM-768 at the TLS
layer is not yet a deployable target.
ForbidClassicalKEM continues to refuse a pure-classical curve at the
application layer; it does NOT refuse a hybrid that includes ML-KEM-768.
Decision recorded in consensus/config commit 12d7000c.
Closes F103 (pure vs hybrid ML-KEM-768 ambiguity).
Expose the chain-wide ChainSecurityProfile to operators, dApps, and
auditors via two surfaces wired through a single boot site
(initSecurityAPI):
- JSON-RPC under namespace "lux" at /ext/lux:
lux_securityProfile → ProfileReply (full profile JSON)
lux_blockSecurity → BlockSecurityReply (chain-wide envelope)
- REST sidecar at /ext/lux/v1/security/{profile,block/{n}}
- Prometheus gauges under namespace "security" on /ext/metrics:
security_profile_post_quantum_end_to_end{profile_id, profile_name}
security_profile_nist_friendly{profile_id}
security_profile_lux_canonical{profile_id}
security_profile_unsafe_mode_enabled{profile_id}
security_mempool_classical_credentials_total{chain}
security_zchain_proof_lag_epochs
The reply shape is SCREAMING_SNAKE canonical names (renderName) so audit
tooling matches on stable identifiers; the underlying scheme bytes come
from consensus/config. A node booted without a SecurityProfile pin
returns ErrNoProfile (RPC) or HTTP 503 (REST) — the legacy networks
keep their classical-compat posture without forging a profile.
Closes the lux_securityProfile and profile-gauges F102 follow-ups.
The X-chain mempool now installs the chain-wide credential-admission
policy resolved from genesis at node bootstrap (F102 close-out). Wiring
path:
genesis → node.SecurityProfile() → xvm.Factory.SecurityProfile
→ VM.securityProfile → vm.Initialize → xmempool.SetAuthPolicy
X-chain credentials are wrapped in fxs.FxCredential; the mempool gate
unwraps them before consulting the auth policy so the same
EnforceCredentialPolicy implementation gates both P-chain and X-chain.
Integration test exercises the full path: build VM with strict-PQ
profile, call Initialize + Linearize, then issue a tx with a wrapped
classical secp256k1 credential via IssueTxFromRPCWithoutVerification.
Observe ErrLegacyCredentialUnderStrictPQ.
The platformvm mempool now installs the chain-wide credential-admission
policy resolved from genesis at node bootstrap (F102 close-out). Wiring
path:
genesis → node.SecurityProfile() → platformvm.config.Internal.SecurityProfile
→ vm.Initialize → pmempool.SetAuthPolicy
Strict-PQ chains refuse classical secp256k1 credentials at gossip time
via the existing auth.EnforceCredentialPolicy gate; legacy/classical-
compat networks keep admitting them unchanged (nil profile is a no-op
gate).
Integration test exercises the full end-to-end path: build VM with
SecurityProfile = LuxStrictPQ(), call Initialize, attempt to add a tx
with an unwrapped *secp256k1fx.Credential, observe
ErrLegacyCredentialUnderStrictPQ.
Closes the CPX deferred item:
"chain-builder code that constructs the mempool today does not call
SetAuthPolicy ... follow-up that depends on plumbing the
*config.ChainSecurityProfile from genesis into the chain builder"
The reusable hanzoai/.github/docker-build.yml requires cross-org callers
to override runner labels — hanzoai org scale-sets aren't visible from
luxfi org. All v1.26.x CI runs have failed with startup_failure since
the upstream workflow added this requirement (~2026-05).
Per the reusable workflow's docstring:
Cross-org callers (luxfi/*, zooai/*, liquidityio/*) MUST override
these with their own org's ARC scale-set labels.
Switch to lux-build-linux-{amd64,arm64} labels which exist on the
luxfi ARC scale-set.
Adds Node.SecurityProfile() getter and Node.initSecurityProfile() boot
step. New() now calls initSecurityProfile() right after
initBootstrappers() and before tracer/metrics/networking/chain init —
every downstream consumer (signer factory, peer handshake gate,
mempool, validator registry, bridge oracle) sees the resolved
*consensus/config.ChainSecurityProfile (or its absence) via the
getter at construction time.
The pin lives in genesis.Config.SecurityProfile (luxfi/genesis@v1.9.6,
which pins luxfi/consensus@v1.23.5). Resolution:
1. genesiscfg.GetConfig(networkID) — load JSON-form genesis
2. cfg.SecurityProfile.Resolve() — refuse mismatched pin
3. stamp result onto n.securityProfile
4. emit startup banner with hash, post-quantum, NIST-friendly,
classical-SNARK, and BLS-fallback posture
A forked binary that swaps in a different canonical profile content
fails Resolve() because the live ComputeHash diverges from the
genesis-pinned hex — closes the F102 attack chain end-to-end.
Adds 5 regression tests for the load + reject paths under StrictPQ
and FIPS profiles, plus nil-pin / wrong-hash / unknown-ID rejection.
Bumps luxfi/consensus v1.23.4 → v1.23.5, luxfi/genesis pseudo →
v1.9.6, luxfi/crypto v1.18.3 → v1.18.4.
Mirrors the P-chain gate (vms/platformvm/txs/mempool) onto the X-chain
(xvm) mempool. The X-chain wraps every credential in fxs.FxCredential,
so the gate unwraps the inner verify.Verifiable before handing the
slice to auth.EnforceCredentialPolicy — the policy package only sees
the canonical *secp256k1fx.Credential type.
Same opt-in shape as the P-chain wiring: SetAuthPolicy installs the
ChainSecurityProfile + ClassicalCompatRegistry; without a profile the
gate is bypassed.
Three integration tests cover: strict-PQ + no registry refuses
classical, no policy admits classical, strict-PQ + empty registry
refuses classical (the all-allow-listed contract is that the registry
names the allowed addresses; an empty registry admits nothing).
Wires the canonical credential-policy gate (vms/txs/auth) into the
P-chain mempool. Adds an opt-in setter so existing callers that have
not migrated their construction path observe no behavioural change —
without SetAuthPolicy, the gate is bypassed and every tx the legacy
path accepted is still accepted.
Once a chain pins a non-nil *config.ChainSecurityProfile via
SetAuthPolicy, every Add path runs through auth.EnforceCredentialPolicy
before the underlying mempool sees the tx. A classical
secp256k1fx.Credential under RequireTypedTxAuth=true returns
auth.ErrLegacyCredentialUnderStrictPQ.
The originator is left at ids.ShortEmpty here because P-chain txs do
not bind a single canonical "from" address (the input owners can name
many keys). Chain builders that supply a ClassicalCompatRegistry whose
IsAllowed depends on tx-level context layer that resolver on top of
this gate.
Three integration tests cover: strict-PQ + no registry refuses
classical, no policy (legacy default) admits classical, classical-
compat profile admits classical. The full P-chain mempool suite
(TestBlockBuilderMaxMempoolSizeHandling et al.) is unchanged.
Adds the canonical credential-policy gate that the P-chain and X-chain
mempools use to enforce a strict-PQ ChainSecurityProfile at admission
time. One package, one entry point (EnforceCredentialPolicy), one
error (ErrLegacyCredentialUnderStrictPQ).
- ClassicalCompatRegistry: read-only allow-list interface. Names a
bounded set of legacy operator addresses that may still post
classical secp256k1 credentials on a chain that otherwise pins
RequireTypedTxAuth=true. The chain's governance pathway is the
only writer; the mempool only reads.
- NewStaticClassicalCompatRegistry: frozen registry constructed from
a fixed []ids.ShortID slice. Useful for tests and for genesis-
pinned allow-lists before the governance path is online.
- EnforceCredentialPolicy: idempotent gate, branches on
profile.RequireTypedTxAuth:
false → admit unconditionally (classical-compat profile)
true → walk creds; if any is *secp256k1fx.Credential and the
originator is not in the registry, return
ErrLegacyCredentialUnderStrictPQ.
10 tests cover: nil profile (ErrNilProfile, programmer error), classical
profile admits, strict-PQ admits PQ-only creds, strict-PQ refuses
classical without registry, strict-PQ refuses originator not in
registry, strict-PQ admits allow-listed originator, mixed creds with
one classical refuses, empty creds always admits (syntactic verifier's
job to require ≥1), nil-safe staticRegistry, distinguishes addresses.
Wiring into platformvm/txs/mempool and xvm/txs/mempool lands in the
follow-up commits.
Re-exports the canonical ML-DSA-65 (FIPS 204) UTXO feature extension
from github.com/luxfi/utxo/mldsafx so platformvm and xvm callers depend
on a single import path:
github.com/luxfi/node/vms/mldsafx
One feature extension, one import path. The wire types (Credential,
TransferInput/Output, MintOutput, MintOperation, OutputOwners, Input,
Fx, Factory) and the security-level constants are type aliases against
the upstream package — adding mldsafx as a node-side bridge introduces
zero runtime code.
This is the foundation the P-chain and X-chain mempool admission gates
(vms/txs/auth) and the strict-PQ tx variants build against.
The prior commit referenced luxfi/consensus v1.23.3, but that tag was
already in use on the remote. Retagged the new ChainSecurityProfile
ValidatorSchemeID work as v1.23.4 and update node's go.mod/go.sum to
match. luxfi/ids v1.2.10 is unaffected.
NodeIDScheme enum (MLDSA65=0x42 canonical, Secp256k1=0x90 classical-
compat-unsafe only). NodeID derivation now domain-separated via
SHAKE256-384("LUX_NODE_ID_V1" || chain_id || scheme || pubkey).
Wire encoding includes a leading scheme byte so the receiver can
verify without trusting the profile alone.
Strict-PQ profile rejects secp256k1 NodeIDs.
Classical-compat profile accepts both (transition path).
Migration: hardfork activation switches strict-PQ chains from
mixed-scheme to ML-DSA-only at a configured activation block.
network/peer/scheme_gate.go is the single primitive consumers funnel
inbound NodeIDs through: SchemeGate.Classify(nodeID, scheme, height,
site) returns a TypedNodeID once the chain policy admits the pair.
The ActivationHeight field implements the hardfork transition window;
ClassicalCompatUnsafe mirrors the operator opt-in flag (refused under
strict-PQ regardless, honoured on permissive).
Existing 20-byte ids.NodeID array stays byte-identical for storage /
map keys / codec; the scheme byte travels alongside it on the wire
via ids.TypedNodeID. Consumers of the 20-byte form (peer/upgrader,
proposervm block proposer, platformvm validator registry, mempool
sender) continue to work unchanged at the storage layer; the
SchemeGate boundary is what stamps the scheme byte and runs the
cross-axis check.
Bumps luxfi/ids v1.2.9 -> v1.2.10 (NodeIDScheme, TypedNodeID,
DeriveMLDSA, FullDigest, error surface) and luxfi/consensus
v1.23.2 -> v1.23.3 (ValidatorSchemeID accessor, AcceptsValidatorScheme,
ErrValidatorSchemeMismatch).
Tests:
- TestSchemeGate_NewSchemeGate_RejectsNilProfile
- TestSchemeGate_StrictPQ_AcceptsMatchedScheme
- TestSchemeGate_StrictPQ_PostActivation_RejectsClassical
- TestSchemeGate_StrictPQ_PreActivation_AcceptsBothSchemes
- TestSchemeGate_StrictPQ_PreActivation_RejectsClassicalAfterCutover
- TestSchemeGate_Permissive_AcceptsClassicalUnderUnsafeFlag
- TestSchemeGate_RejectsUnknownSchemeByte
- TestSchemeGate_PinsProfileScheme
- TestSchemeGate_SiteTagIncludedInError
Patch-bump: v1.26.9 -> v1.26.10.
Replaces classical X25519/ECDH peer handshake with ML-KEM-768 (default)
and ML-KEM-1024 (high-value validator/DKG channels). Node identity is
signed with ML-DSA-65 over a TupleHash256-bound transcript.
cSHAKE256 derives the AEAD session key with customization
"LUX_NODE_AEAD_V1", binding profile_id, chain_id, both nodes' ML-DSA
public keys, and the shared KEM secret.
DKG channels in pulsar/pulsar-m force ML-KEM-1024.
Strict-PQ profile refuses peers offering X25519Unsafe KEM.
Patch-bump.
Only P-Chain (chain manager startup) and X-Chain (UTXO infra) are
required primary-network chains. C-Chain (EVM), D-Chain (DEX),
B-Chain (Bridge), T-Chain (Threshold) — and any subnet blockchain —
must be explicitly tracked via --track-chains. A validator gets exactly
the topology its operator asks for, nothing more.
Pre-fix: every primary-network blockchain in genesis was created
unconditionally because `constants.PrimaryNetworkID != tx.ChainID`
short-circuited the TrackedChains gate. Liquidity validators (no
C-Chain plugin shipped) ran into "failed to initialize VM: parsing
genesis: unexpected end of JSON input" on every health check.
Replace the primary-network bypass with a per-VM check: only
tx.VMID == constants.XVMID skips the gate. Everything else (including
C-Chain on primary network) goes through TrackedChains.
Two related fixes that make chain tracking explicit per-validator:
1. config/config.go: drop the silent default
`if TrackedChains == nil { TrackAllChains = true }`. That made any
node without an explicit --track-chains list try to spin up every
chain in P-chain state, including ones whose VM plugin wasn't
loaded. The setting was a footgun. Empty TrackedChains now means
"track only P/X (built-in)". TrackAllChains stays as an explicit
opt-in escape hatch.
2. chains/manager.go: when buildChain returns ErrNotFound (VM plugin
not registered for this chain's vmID), treat it as the validator's
"I don't validate this one" signal — log info, mark the slot
bootstrapped, and return WITHOUT registering a per-chain failing
health check. The chain stays in pending state for hot-load.
Previously this registered a permanent 503 on /ext/health for the
chain-alias, which made kubelet liveness probes kill any pod that
didn't ship plugins for every chain in the primary genesis. Now
validators participate per-plugin: load liquid-evm/dex/fhe → those
chains validate; don't load Lux's upstream EVM plugin → C-Chain
stays in pending without crashing the node.
Real failures (e.g. genesis decode error, db corruption) still register
the failing health check — those are operator misconfigurations, not
deliberate opt-outs.
Future: a Lux-signed featured-chains.json + --track-featured (default
true) would make this opt-in safer at scale — limits the DDoS surface
of unbounded chain creation while still letting validators earn yield
on the curated set out of the box.
Adds platform.getChains alongside platform.getNets — same wire shape
(IDs in, list of {ID, ControlKeys, Threshold} out), names that match
the user-facing concept ("chain") instead of the internal "net" /
"subnet" jargon. APIChain replaces APINet, ClientChain (alias for
ClientNet) keeps drop-in source compatibility.
Why now: GetNets was already marked deprecated via the "deprecated
API called" log line, but no canonical replacement existed — callers
had to keep calling the deprecated method. This commit lands the
replacement so the bootstrap binary's chain idempotency check
(EnsureChainNetwork → list chains, match owner set) can target the
canonical name.
GetNets stays available indefinitely for wire-protocol compatibility.
GetChains delegates to GetNets internally so the two methods can
never diverge — bug-fix one, both fix.
Test pinned in service_test.go alongside GetNets test (existing file
has //go:build skip but the test is documentation for the contract).
createCChainIfNeeded is the one-time recovery path for the historical
mainnet migration where post-merge geth state was imported under a fixed
blockchainID (no CreateChainTx). On every other launch the function
either silently no-ops on the missing data dir or — worse — risks
touching the filesystem before the data dir exists during a fresh
genesis. Gating it behind LUX_MIGRATE_CCHAIN=1 keeps the migration path
exactly where it belongs: opt-in, run once, never again.
The wire protocol uses two flags on responses:
MsgResponseFlag (0x80) — set on EVERY response (success or error)
MsgErrorFlag (0x40) — set ONLY on error responses
The transport's readLoop already converts MsgErrorFlag responses into a
non-nil err out of Conn.Call, so by the time the client checks respType
the response is guaranteed successful. Treating MsgResponseFlag as the
"is this an error?" predicate fired on every successful Initialize and
rendered the binary InitializeResponse payload (LastAcceptedID, parent,
height, bytes, timestamp) as if it were an error string, producing
"vm error: <unprintable bytes>" and crashing chain creation despite the
plugin reporting "VM initialized successfully".
Symptom in the field: lqd repeatedly logged
failed to create chain on net 11111111111111111111111111111111LpoYY:
failed to initialize VM: zap initialize: vm error: \x00\x00\x00 ...
while the plugin's own log read "ZAP VM initialized successfully" and
"Chain initialized successfully". Health check C reported permanent
failure, eth_getBalance returned 404, no blocks were ever produced.
Fix: drop the spurious MsgResponseFlag check and strip both flags before
comparing the message type so that any well-formed response is accepted.
Regression test in client_initialize_test.go pins both halves of the
contract: a success response is decoded (lastAcceptedID round-trips),
and a server-side error is surfaced through err with the original
message intact.
Picks up luxfi/genesis@2b16f454 — adds operator-overridable C-Chain
genesis at the embedded-loader layer. Pairs with d0a248f5 (this repo's
LUX_AUTOMINE_CCHAIN_GENESIS_PATH for the automine single-node path).
With both env vars wired, downstream networks can reuse stock lqd
without forking configs/network/cchain.json or patching this binary.
The automine path embeds the C-Chain genesis bytes into the primary-
network genesis at boot. Until now that genesis was a hardcoded constant
(chainId 31337) — fine for stock lqd local dev, but downstream networks
(Liquidity at chainId 8675312, etc.) had no way to override without
patching the binary. The chain-config-dir scan happens AFTER the EVM
plugin has already initialised from the embedded bytes, so dropping a
genesis.json into configs/chains/C/ is silently ignored.
Adds LUX_AUTOMINE_CCHAIN_GENESIS_PATH: when set, lqd reads the JSON at
that path and embeds it as the C-Chain genesis. Unset → unchanged
behaviour (chainId 31337 default).
The saved AutomineNetworkConfig also mirrors the resolved value so the
on-disk record is true to what was embedded.
Match the OPT-IN comment at genesis/builder/builder.go:617. The
initChainManager path was hard-erroring when the platform genesis
had no constants.EVMID chain — that contradicted the documented
opt-in behaviour and forced consumers (Liquidity Network) to bake
a placeholder EVM into the primary genesis just to satisfy the
node init.
With this change, networks that register their own EVM as a
dedicated chain via platform.createChainTx (the canonical L2-on-Lux
or sovereign-chain flow) leave the platform genesis EVM-less and
cChainID stays ids.Empty. Chain manager + aliasing already accept
the empty ID — no further changes needed.
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.
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.
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.
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.
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.
* 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.
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.
* 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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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).
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.
| `txs.Parse(v0)` in default mode | continues to work — byte-preserving migration contract is preserved (the gate is NOT wired into platformvm/txs.Parse; it lives at the new ZAP-vs-legacy wire dispatcher Blue is building) | PASS |
The decision NOT to gate `txs.Parse` directly is intentional: that
entry point owns the byte-preserving v0→TxID invariant; gating it
would break any validator bootstrapping from pre-activation history.
The gate's enforcement point is the new wire dispatcher
(`zap_native.IsZAPBytes` / `.ShouldUseZAPForWrite`) Blue is wiring
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.