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 (07bb303044 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 <tenant> 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 <tenant> 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 409297a089 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>
* 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 c431d884ed (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
1b3651ccf6 (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 c431d884ed (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).