The KMS fetch was migrated to the canonical /v1/kms/orgs/{org}/secrets path, but
the file header and the usage example were left describing the Infisical v3 raw
API and telling operators to export KMS_WORKSPACE_ID — a variable this program
does not read. Following the header got you a tool that could not resolve the
org and failed with no obvious reason.
Both now match resolveMnemonic: KMS_ENDPOINT + KMS_TOKEN + KMS_ORG, fetching
GET ${KMS_ENDPOINT}/v1/kms/orgs/<org>/secrets/lux/<env>/staking/mnemonic.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
fetchKMSMnemonic called Infisical v3 raw secrets:
GET ${KMS_ENDPOINT}/api/v3/secrets/raw?workspaceId=..&environment=prod&secretPath=..
That endpoint is not served by kms.lux.network or kms.hanzo.ai. Both answer it
with the console SPA HTML, so the JSON decode failed and no mnemonic was ever
retrievable this way — while the function, its env vars and its log line all
called it KMS.
Now it speaks the one documented HTTP surface, which luxfi/kms describes as the
only way to read a secret:
GET ${KMS_ENDPOINT}/v1/kms/orgs/<org>/secrets/lux/<env>/staking/mnemonic
Authorization: Bearer ${KMS_TOKEN} (IAM-signed, resolved to <org>)
-> {"secret": {"value": "..."}}
The server splits the path at its last slash into (path, name), so the secret
path still disambiguates the luxd env. ?env= is the KMS environment slug and
defaults to "default", matching the luxfi/kms client KMS_ENV.
KMS_WORKSPACE_ID is gone — an Infisical concept with no meaning here. Scope is
the org in the path: KMS_ORG, defaulting to lux. No compat shim; the old call
never worked.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Drop the Avalanche-heritage /ext prefix; /v1 is the single canonical route
surface (one way, no backward compat). The node's baseURL is the source of
truth; clients, SDKs, CLI, indexer, maker, genesis, netrunner, and the
k8s/compose/gateway/explorer configs are updated to match.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
--brands=hanzo,zoo issues CreateChainTx only for the named brands, in
canonical list order (deterministic chain ordering). Unknown brand names
are a hard error (a typo must not silently create a different chain set).
Pure stdlib; zero new deps. Default (empty) = all five brands as before.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
bootstrap-chain defaulted --track-chain-ids=hanzo,zoo,pars,spc — it created
brand L2 EVMs INSIDE luxd, the dead WAY-1 that produced the phantom brand
chains which decayed into dangling chain-aliases. Per brand_sovereign_node_one_way
luxd runs the LUX primary network ONLY; brands run their own sovereign node.
- default --track-chain-ids is now empty
- hard-refuse brand names (hanzo/zoo/pars/spc/osage) before any P-chain spend
- doc updated; retires alongside cmd/createchaintx (deleted on
decomplect/retire-brand-l2-in-luxd)
Network reboot, no backward compat: delete the AI-slop pqcrypto bundle + every
gate pinning it (pqcryptoConfig genesis activation, relaunch-safety test pin).
ML-KEM stays via standalone mlkem (0x012201). pqcryptoConfig removed from all
upgrade.json/yaml; 0x9003 retired, never reuse.
Picks up the 2026-06-06 relicensing:
- luxfi/vm v1.2.0 → Lux Ecosystem License v1.2 (was Lux Research v1.0).
License-only retag of the extracted runtime; patent reservation
preserved for runtime optimization surfaces, royalty-free for
Descending Chains.
- luxfi/sampler v1.1.0 → BSD-3-Clause (was Lux Research v1.0).
License restored to match the luxfi/node provenance (originally
extracted from node/utils/sampler).
- luxfi/staking v1.5.0 → BSD-3-Clause (was Lux Research v1.0).
License restored to match the luxfi/node provenance.
No code changes in this commit — go.mod/go.sum only.
Required for bootstrap-chain wire-compat with luxd v1.28.29. The
running node's wallet/network/primary uses utxo.ParseUTXO ZAP
dispatcher; the previous v0.3.4 emitted incompatible TypeKind/ShapeKind
discriminators (`wire: ShapeKind discriminator does not match expected
primitive shape`). Bump aligns the wire format.
Also adds local-codec replace because the published luxfi/codec@v1.1.4
does not contain the zapcodec package — that landed locally for the
2026-06 Wave 1D refactor and luxfi/node references it via the
zapcodec dispatch path.
inventory/brand-l1-2026-06-05-reset.json: post-reset canonical brand
L1 IDs. Supersedes brand-l1-2026-06-05.json (initial deploy with
orphan-prone naming).
Symmetric with cmd/rlp-import. Thin HTTP client that:
1. Idempotency precheck reads <output-file>.sentinel on the local PVC
and short-circuits when Status=done + range matches — luxd is never
contacted in the warm-cron case.
2. Health-probes ${LuxdRPC}/ext/health (30s cap).
3. POSTs admin_exportChain via /ext/bc/${chain}/rpc with KMS_AUTH_TOKEN
bearer auth (same env-var contract as rlp-import).
4. Maps Status → exit code:
ok | noop → 0
interrupted → 4 (operator re-runs with from-height=HighestExported+1)
bad input → 1
luxd unreach → 2
admin RPC err → 3
Lives in the cmd/ nested module per the existing rlp-import precedent.
Builds with GOWORK=off; Makefile target `make rlp-export` does this for you.
Tests cover: flag parsing, "latest" resolution, all four idempotency
fall-through branches, happy path + noop + interrupted server responses,
luxd unreachable, admin RPC error, KMS_AUTH_TOKEN bearer header
plumbing. 14 tests, all green.
Symmetric Makefile additions: `make rlp-export`, `make rlp-import`,
`make cmds`, `make test-rlp-export`.
Standalone Go binary that runs the six-step tenant RLP import lifecycle
in a single process: sentinel check → fetch → sha256 verify → wipe
chainData → POST admin.importChain → touch sentinel. Replaces the shell
loop split between luxd-startup.yaml's main container and the operator's
tenant-import init container.
The binary is invoked by luxfi/operator as a K8s Job — one per
LuxNetwork.spec.tenantImports[i]. Exit codes are wired for K8s backoff:
1=bad input/SHA mismatch/404, 2=luxd unreachable (transient retry),
3=admin.importChain RPC error. Content-hash addressing matches
TenantImportSpec.ContentHash byte-for-byte so the operator and the
binary compute identical sentinel paths.
Tests cover all six lifecycle steps + every failure exit code.
The earlier MNEMONIC unification commit (4383ad4) missed this CLI tool;
it was still reading the legacy MNEMONIC env name directly via
os.Getenv. Per the "one var, one way" rule the canonical name is
LUX_MNEMONIC across every code path in the genesis repo.
Updated in cmd/bootstrap-chain/main.go:
- Package-doc usage example: MNEMONIC="..." -> LUX_MNEMONIC="..."
- --print-addr-only flag help: scrub MNEMONIC -> LUX_MNEMONIC
- --key-file flag help + comments: MNEMONIC -> LUX_MNEMONIC
- loadKey() reads os.Getenv("LUX_MNEMONIC") instead of "MNEMONIC"
- Error message references LUX_MNEMONIC
No backcompat fallback: callers using the legacy MNEMONIC env get the
clear "LUX_MNEMONIC env var or --key-file required" error.
Verification:
- go build ./... -> clean
- go vet ./... -> clean
- go test ./... -count=1 -> ok configs, ok pkg/genesis
Comment-only scrub of upstream-fork residue in user-facing docstrings,
CLI help text, and LLM.md prose. No behavior change; no wire-format
change.
pkg/genesis/keys.go:
- DefaultNumAccounts doc: drop "AvalancheJS" from canonical-BIP44 wallet
list (was "Core, MetaMask, AvalancheJS, lux key derive" -> now
"Lux Wallet, MetaMask, lux key derive").
- LoadKeysFromMnemonic doc: drop "Avalanche/Lux BIP44 path" (just
"Lux BIP44 path") and "Core Wallet, AvalancheJS" wallet list (now
"MetaMask, Lux Wallet").
- LoadKeysFromMnemonic inner loop comment: same scrub
("Standard Avalanche/Lux secp256k1 child" -> "Standard Lux ...";
"matches Core Wallet, MetaMask, cast" -> "matches Lux Wallet, ...").
- BuildBIP44WalletAllocations doc: drop "SLIP-0044 coin type Lux
inherits from its upstream lineage" prose. Replaced with brand-
neutral "BIP44 coin type Lux uses by canonical convention".
cmd/genesis/main.go:
- bip44-wallet-keys flag help: drop "SLIP-0044 coin type 9000" ->
"BIP44 coin type 9000 canonical convention".
LLM.md:
- "Fortuna Timestamp Fix" prose: drop "Fortuna, Etna, and Granite
upgrades" naming. Replaced with "pre-Quasar Edition upgrades"
framing. The JSON field names (etnaTimestamp / fortunaTimestamp /
graniteTimestamp) are preserved in the example block because they
are the wire format consumed by luxfi/evm.
Intentionally kept (per task scope and forward-compat realities):
- params_test.go:33 + params.go:303 "avax HRP" rejection guards —
these are protective security tests/comments, not residue. The whole
point is to refuse "avax"-HRP bech32 addresses synthesized from a
canonical lux validator's 20 bytes. Stripping the "Avalanche" prose
would obscure why the guard exists.
- pkg/genesis/types.go:229 EtnaTimestamp uint64 field — this Go field
is a downstream mirror of the wire-format identifier defined at the
source of truth in luxfi/evm/params/extras/network_upgrades.go and
consumed by ~95 JSON genesis blobs, K8s startup manifests, academy
docs, and migration tooling. Renaming the genesis-repo mirror alone
would break JSON round-trip with every other consumer in the
ecosystem. A wire-format rename of EtnaTimestamp -> QuasarTimestamp
must originate in luxfi/evm and migrate every embedded genesis
config + every k8s manifest + every doc in one coordinated PR series.
That migration is OUT OF SCOPE for this commit and is reported back
to the dispatcher; see external-caller surprises below.
Verification:
- go build ./... -> clean
- go vet ./... -> clean
- go test ./... -count=1 -> ok configs, ok pkg/genesis
- grep '[Aa]valanche\|AvalancheJS\|Core Wallet\|SLIP-0044' pkg/genesis
cmd LLM.md
-> only 2 lines remain (params.go:303 + params_test.go:33),
both inside the protective HRP-rejection guard prose.
One env, one way. The genesis mnemonic is now read from exactly one
env var: LUX_MNEMONIC. The prior two-env fallback chain
(MNEMONIC > LIGHT_MNEMONIC) is gone. Per-env isolation is by a
DIFFERENT mnemonic per env (loaded from KMS), not by env-var name.
pkg/genesis/keys.go:
- New MnemonicEnvVar = "LUX_MNEMONIC" constant.
- getMnemonicEnv() reads only LUX_MNEMONIC; no fallback.
- LightMnemonic constant kept (it's the well-known dev seed VALUE;
pass it as the value of LUX_MNEMONIC for local nets).
- IsProductionNetwork docstring updated to spell out the canonical 4:
mainnet(1) / testnet(2) / devnet(3) refuse public mnemonics;
local(1337)+ allows LightMnemonic.
- Docstring rot purged: the prior "50M free + 50M vesting at 1%/year
over 100y" claim was a lie — buildConfigFromKeyInfos never wired the
100-period schedule (the code admits the schedule "overflows zapdb
batch limit"). Replaced with the truth: 1000 × 50M LUX on X-Chain
AND P-Chain, immediate spend; C-Chain is treasury-only (2T LUX).
- Dead code deleted: StakingStartTime, UnlockInterval consts,
buildUnlockSchedule() function.
pkg/genesis/allocations.go:
- VestingConfig struct removed.
- DefaultVesting() removed.
- WithVesting() builder method removed.
- Vesting branches in PChain() / PChainMap() collapsed to immediate-only.
- MainnetAllocations no longer calls WithVesting(DefaultVesting()).
pkg/genesis/validator_keys.go:
- VestingPeriods const removed.
- GeneratePChainAllocationsWithVesting() removed.
pkg/genesis/networks.go (new):
- Canonical primary-network-id table: MainnetID / TestnetID / DevnetID
/ LocalID = 1 / 2 / 3 / 1337. Matching C-Chain (EVM) chain IDs:
96369 / 96368 / 96370 / 31337.
- IsCanonicalPrimaryNetwork helper.
cmd/checkkeys/main.go:
- Reads LUX_MNEMONIC via genesis.MnemonicEnvVar (no fallback).
cmd/derive100/main.go:
- Usage hint updated: mnemonic-env: LUX_MNEMONIC.
cmd/genesis/main.go:
- Flag help, env-var docs, examples, and error messages all reference
LUX_MNEMONIC.
pkg/genesis/light_mnemonic_guard_test.go:
- All t.Setenv calls use LUX_MNEMONIC.
- Test labels reference LightMnemonic (the constant) not the dead env name.
configs/getgenesis_test.go:
- Comment updated: LightMnemonic instead of LIGHT_MNEMONIC.
CHANGELOG.md + LLM.md:
- Documented the unification + the dead-code removal.
The validator stake-lock (3-entry 5y/10y/20y schedule attached to the
validator's UTXO inside buildConfigFromKeyInfos at keys.go:1077-1083)
is unchanged — that locked-stake bucket is the only UnlockSchedule in
the canonical genesis path and the ProtocolVM needs it.
External-caller sweep: grepped ~/work/lux, ~/work/hanzo, ~/work/zoo,
~/work/luxfi for genesis.VestingConfig / genesis.DefaultVesting /
genesis.GeneratePChainAllocationsWithVesting / genesis.StakingStartTime
/ genesis.UnlockInterval — zero hits. Safe to delete. (luxfi/keys at
~/work/lux/keys has its own MainnetAllocations copy — separate repo,
not affected.)
Verification:
- go build ./... → clean
- go vet ./... → clean
- go test ./... -count=1 → ok configs, ok pkg/genesis
- grep '[^_X]MNEMONIC\|LIGHT_MNEMONIC' under pkg/genesis cmd configs →
zero matches (only CHANGELOG history + Python script local vars).
- grep VestingConfig|buildUnlockSchedule|GeneratePChainAllocationsWithVesting
under source → zero matches.
The CreateChainTx `name` argument is a P-chain-unique identifier — caller's
choice. The brand alias ("hanzo", "zoo", "spc", "pars") is user-facing and
belongs at the chain-aliases ConfigMap layer. Conflating the two forced
the {brand}fix-{env}/ symlink workaround when re-bootstrapping over stale
P-chain chain rows from prior CreateChainTx attempts.
Default chain.name is now content-addressed: "<brand>-<8hex>" where
8hex = hex(SHA256("<brand>|<env>|"||genesisBytes))[:4]. Properties:
- Idempotent: same canonical genesis → same chain.name → re-runs detect
the existing chain via platform.getBlockchains and skip CreateChainTx.
- Collision-free: different genesis bytes (any change to chainId, alloc,
config) → different chain.name → CreateChainTx succeeds against
P-chain instead of colliding with the previous stale row.
- Brand-rooted: "hanzo-3f4a1c8b" stays humanly legible without conflating
the chain.name with the user-facing brand alias.
Spec struct renamed: Name → Brand (user-facing) + ChainTxName (P-chain ID).
Result JSON gains "brand" and "chainTxName" fields so downstream
consumers (chain-aliases CM, explorer-chains CM, gateway routes) can pin
on the brand value without parsing the chain.name suffix.
Override path: --chain-name-override "<brand>=<name>,..." pins explicit
names for legacy reattachment scenarios.
4 new tests pin the contract:
- Deterministic (same input → same output)
- DifferentGenesisDifferentName (bytes change → name changes)
- DifferentBrandDifferentName (brand-scoped)
- DifferentEnvDifferentName (env-scoped)
All 14 tests pass.
Close the Track D root cause that produced empty 0xa6aecc6c... state
roots on testnet brand L2s (hanzo, zoo, spc, pars). Pre-existing brand
L2 JSONs under configs/<brand>-<env>/genesis.json are already clean,
but nothing prevented a future generator (or a hand-edit cribbed from
the primary network's testnet/genesis.json embedded cChainGenesis blob)
from reintroducing those fields.
The fields override the result of luxfi/geth/core/genesis.go::
Genesis.Commit's flushAlloc — a stale value silently displaces the
canonical alloc trie, then the L2 boots with block 0 stateRoot pointing
at a trie node that was never persisted. That's the exact failure mode
producing "missing trie node 0x2d1cedac..." errors on the existing
testnet hanzo L2.
Changes:
- cmd/bootstrap-chain/main.go: hard-refuse any genesis carrying a
non-empty stateRoot or genesisHash before any CreateChainTx burns
LUX. Error names the specific geth/core/genesis.go lines and the
Track D context so the operator can fix at source.
- cmd/bootstrap-chain/main_test.go: TestBrandL2GenesisHasNoStateRoot-
OrGenesisHash walks every <brand>-<env>/genesis.json under configs/
and asserts the absence of both fields. Test self-checks that at
least one brand L2 dir was found so a path bug can't silently pass.
The historical Lux primary C-Chain header workaround that needed these
overrides is dead — the empirical block-0 hash now lives in
configs/mainnet/cchain.json. Brand L2 JSONs MUST stay clean.
Authorized by 2026-06-02 destructive recovery sweep — Track D root
cause closure.
Operators that already hold a 32-byte hex secp256k1 deployer key on disk
(e.g. ~/work/lux/keys/deployer-test-dev/private.key used to fund the
test+dev primary genesis allocations) can pass --key-file <path> instead
of MNEMONIC + --bip44-idx. MNEMONIC path remains the default.
Drove the brand-L2 CreateChainTx registration for testnet + devnet
(closes universe #168) — 8 chains, all bootstrapped 3/3 pods per env.
go.mod: add local replace github.com/luxfi/node => ../../node to pick up
the wallet's P-only fail-soft (lux/node@HEAD). Drop the replace once
luxfi/node tags the change. tidy refreshed transitive deps (accel v1.1.4
→ v1.1.8, local genesis v1.12.19 → v1.13.8) — these are no-op for the
bootstrap-chain logic.
Pre-v1.28.0 nodes wrote P-chain UTXOs at wire-version=0. v1.28.0
ships the multi-version codec (luxfi/node@c589251a1d) and writes
all new UTXOs at wire-version=1 by default. The bootstrap-l2 tool
embeds the luxfi/node wallet to build/sign CreateNetworkTx and
CreateChainTx; the v1.27.24 pin's wallet codec only knew v0, so
syncing the wallet against a fresh v1.28.0 node failed with:
wallet sync: unknown codec version
Bump cmd/go.mod to luxfi/node v1.28.0, which pulls in the v0+v1
codec register and decodes a v1.28.0 node's UTXOs correctly.
Verified on lux-devnet: rebuilt bootstrap-l2 then ran:
/work/bootstrap-l2 -uri=http://luxd-0.luxd-headless.lux-devnet.svc.cluster.local:9650 \
-hrp=dev -bip44-idx=5 -track-chain-ids=hanzo,zoo,pars,spc \
-configs-dir=/work/configs -chain-settle-delay=180s -probe-timeout=1800s
All 4 L2 EVMs bootstrapped:
hanzo → i3AxuEfLAqzAvabgpYvXC7QCvqXnJAHr3AYiVzfK2FDauyJSa
zoo → 5YP3iV6ytRjtscF8dcXWQTqd8dwjAaoZnW6oNVfDM3d2mwwvS
pars → JxDPdhGde9PSrZ3nLwe7shwZaXEFBoAJ8V9rGqbcKXy9FEQEm
spc → LHU4rNBLr1Zi2pNJXL47djkCmmyDSgTXmaB4oNQBYyGfqPtg9
Transitive bumps:
github.com/luxfi/consensus v1.25.0 → v1.25.2
github.com/luxfi/genesis v1.12.15 → v1.12.19
* decomplect: move SecurityProfile Resolve to pkg/genesis/security
pkg/genesis core types (SecurityProfile struct) stay free of
luxfi/consensus dep. The verification gate (ResolveProfile + hash
comparison) moves to pkg/genesis/security which imports both.
Result: downstream consumers that only need to parse canonical
genesis data (luxd v1.23.x line, tools, indexers) can do so without
dragging luxfi/consensus + luxfi/threshold + luxfi/validators
(Corona→Corona refactor) into their go.mod.
go list -deps ./pkg/genesis → no luxfi/consensus.
go test ./pkg/genesis/security/ → all 5 F102 verification tests pass.
Closes the dep cascade that blocked the v1.23.x backport of
evmAddr/utxoAddr canonical genesis. One and one way only:
data lives in pkg/genesis, verification lives in pkg/genesis/security,
consensus consumes both — never the reverse.
Closes#93 Phase 1.
* decomplect: split cmd/ into nested module (no luxfi/node in root go.mod)
The Phase 1 split (SecurityProfile struct in core, Resolve in
pkg/genesis/security subpackage) addressed package-level dep cycles,
but the genesis root go.mod still required luxfi/node v1.27.24
because cmd/bootstrap-l2 + cmd/derive100 import node directly for
wallet/tx-builder bits.
This commit moves cmd/ to a nested module
(github.com/luxfi/genesis/cmd) with its own go.mod that holds the
luxfi/node dep. Root go.mod no longer requires luxfi/node, breaking
the MVS cascade that was forcing luxd v1.23.x consumers to upgrade
28 packages just to bump genesis.
Result:
- go list -m all on root no longer surfaces luxfi/node directly
(still transitively visible at v1.23.x-compatible versions, not the
v1.27.x bloat)
- cmd/ retains the same source layout; tools still build via cmd/go.mod
- pkg/genesis core API unchanged
- pkg/genesis/security gate unchanged
Two-module separation:
- github.com/luxfi/genesis — data types + verification primitives
- github.com/luxfi/genesis/cmd — tools that need luxfi/node
One direction, no reverse import.
Closes the structural blocker for v1.23.x backport of canonical
evmAddr/utxoAddr.
Per CLAUDE.md "translate upstream Lux's 'subnet' out immediately":
zero remaining `subnet|Subnet|SUBNET` occurrences in lux/genesis Go +
JSON files. All eleven were comment-level cruft or one struct field
that hadn't been renamed yet.
## Changes
`cmd/bootstrap-l2/main.go`:
- Top-of-file vocabulary block updated — kills the "upstream
CreateSubnetTx" + "ConvertSubnetToL1Tx" framing in favor of the
canonical three-name trinity: `chainID` (chain-owner network),
`blockchainID` (blockchain's own ID), `evmChainID` (EIP-155).
- `platformBlockchain.SubnetID` → `ChainID`; JSON tag
`"subnetID"` → `"chainID"`.
- `resultChain.ChainOwnerID` → `ChainID`; JSON tag
`"chainOwnerId"` → `"chainId"`.
- Inline struct in `existingByName` map: `ChainOwnerID` →
`ChainID`, log line reads `chainID=` instead of `ownerID=`.
- All callsites updated.
`cmd/genesis/main.go`, `pkg/genesis/keys.go`,
`pkg/genesis/allocations.go`:
- Four comment-level rephrases: "subnet-bootstrap CLI" → "chain-
bootstrap CLI", "subnet creation operations" → "chain creation
operations".
## Output JSON wire change
The bootstrap result file emitted by `--output` now uses
`{chainId, blockchainId, evmChainId}` per chain instead of
`{chainOwnerId, blockchainId, evmChainId}`. Downstream consumers
(universe operator, chainboot, anything that reads
devnet-l2-live.json) MUST update their parsers — this is a
breaking wire change in the file format, intentional.
The platform.getBlockchains JSON-RPC method's wire format stays
upstream-shaped on the way IN; our parser now decodes from
`"chainID"` (post-rename in lux/node `APIBlockchain`). If running
against an older lqd that still emits `"subnetID"`, the field
silently zero-values — which is the correct safety behavior (the
caller will fall through to the create path and the operator gets
a re-create-attempt failure rather than a silent ghost reuse).
## Verified clean
grep -rnE "subnet|Subnet|SUBNET" --include="*.go" --include="*.json"
# zero matches in lux/genesis
Build clean. 9 normalizeAllocKey tests still pass.
* feat(genesis): bootstrap-l2 — durable replacement for /tmp script
Recreates the v1.27.x ConvertSubnetToL1 / CreateChain pipeline previously
maintained as /tmp/bootstrap-l2-v127. New tool at cmd/bootstrap-l2/:
- Reads MNEMONIC from env, derives BIP44 m/44'/9000'/0'/0/<idx>
- For each chain in --chains: IssueCreateNetworkTx → wallet re-sync →
IssueCreateChainTx (vmID nyGCobireNhxFB7iM5bxV74hAY6j9nQX6wizxfWomnMMtztkr)
→ IssueAddChainValidatorTx for every primary validator
- Probes info.isBootstrapped(chain=<bcID>) + eth_blockNumber>0 per chain;
if either misses the deadline the whole tool exits non-zero (no partial
state propagated to YAML/CMs)
- Emits a single JSON document {chains:[{name, subnetId, blockchainId,
evmChainId, firstBlockHex, bootstrappedAt}, ...]} for downstream
YAML/CM updates
- --print-addr-only mode to verify funding before spending
go.mod: bump luxfi/node v1.23.36 → v1.27.21 to match cluster runtime
(devnet runs luxd v1.27.21; pre-rename wallet codecs would produce txs the
nodes can't decode). Only existing genesis use of luxfi/node is the
address formatter — no behavior change for other cmds.
Designed for the four canonical Lux L2 EVMs (hanzo, zoo, pars, spc)
but the chain list is data-driven via --chains and --configs-dir.
* feat(genesis): bootstrap-l2 default VM ID → mgj786NP... (native subnet-evm)
Devnet luxd-0 /data/plugins contains exactly two subnet-evm plugin IDs
(mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6 and
ag3GReYPNuSR17rUP8acMdZipQBikdXNRKDyFszAysmy3vDXE), neither matching the
brand-namespaced alias nyGCobireNhxFB7iM5bxV74hAY6j9nQX6wizxfWomnMMtztkr
recorded in the 2026-05-27 chain-aliases ConfigMap annotation.
That alias required a runtime symlink (created post-startup, lost on PVC
remount) — the same fragile path that's been the historical source of
L2-EVM bootstrap failures. Mainnet's lux-mainnet/luxd-startup.yaml uses
mgj786NP... directly and `cp`'s it as the ag3GR... duplicate; we mirror
that pattern here so the plugin is *always* present without a startup
script writing into a PVC.
The chain-aliases CM annotation about nyGCobir... is now stale and will be
rewritten in the post-bootstrap update.
* feat(genesis): bootstrap-l2 — --existing-subnet-ids to resume after partial bootstrap
When CreateNetworkTx succeeds but the subsequent CreateChainTx fails (or
the tool gets killed mid-flight), the subnet still exists on-chain and
the fee was burned. Re-running the tool without --existing-subnet-ids
would burn another CreateNetwork fee.
Format: --existing-subnet-ids=hanzo:2PkWqv...,zoo:abc...
For each named chain, skip IssueCreateNetworkTx and pass the supplied
subnet ID straight into IssueCreateChainTx. Chains not in the map
continue to create a fresh subnet.
Also surfaces a real operational pattern: load-balanced services like
svc/luxd-headless can pin a kubectl port-forward to a lagging pod whose
state is behind the chain head; the tool must still complete by
re-using the subnet visible on the leader pod.
* feat(genesis): bootstrap-l2 — EVM heartbeat tx + decoupled bootstrap probe
Acceptance criterion 'eth_blockNumber > 0' on a fresh subnet-evm chain
requires injecting the first tx (subnet-evm only seals blocks when a tx
arrives). Adding two new flags:
--evm-heartbeat-key=<hex> LUX_PRIVATE_KEY hex (0x9011E888...).
Sends a 0-value self-tx after the chain
reports info.isBootstrapped=true; the first
tx rolls block 1.
--probe-bootstrap-only Skip the eth_blockNumber>0 wait. Used when
an out-of-band heartbeat (chain-heartbeat
CronJob) will roll block 1 later.
The probe is also split into waitBootstrap + probeChain so the heartbeat
fires after isBootstrapped but before eth_blockNumber checks (otherwise
eth_sendRawTransaction returns 'chain not bootstrapped').
Signing path: luxfi/crypto/secp256k1 → ecdsa.PrivateKey → geth/core/types
SignTx with EIP-155 signer scoped to each chain's evmChainID. EVM address
derivation goes through luxfi/crypto/secp256k1.PubkeyToAddress so we never
import an upstream go-ethereum crypto package.
* feat(genesis): bootstrap-l2 — preset chain ID (chain:subnetID:chainID) for resume
Extends --existing-subnet-ids parser to accept an optional third field
<chainID>. When present, both IssueCreateNetworkTx and IssueCreateChainTx
are skipped; the tool only waits for isBootstrapped, sends the heartbeat
(if --evm-heartbeat-key is set), and probes eth_blockNumber.
This is the post-partial-failure resume pattern: if hanzo's subnet+chain
were created successfully in a prior run but zoo onwards failed (or were
never attempted), pass
--existing-subnet-ids=hanzo:2PkWqv...:rHiiTB...,zoo:abc...
and the tool will skip the hanzo P-chain spend, treat hanzo as already
created, and continue with the next chain.
Also gates the AddChainValidatorTx and post-chain wallet re-sync on
presetChainID being unset, since both are unnecessary in resume mode.
* fix(genesis): canonical 0x prefix on all devnet alloc keys + fail-loud bootstrap-l2 (#72)
Two-part fix for the alloc-key shape that bootstrap-l2 consumes:
## 1. Genesis configs patched at source (4 files)
Every alloc key in:
- configs/hanzo-devnet/genesis.json
- configs/pars-devnet/genesis.json
- configs/spc-devnet/genesis.json
- configs/zoo-devnet/genesis.json
now matches the canonical /^0x[0-9a-fA-F]{40}$/ shape. The EVM
genesis loader rejects unprefixed keys; previously bootstrap-l2's
in-memory repair was masking the issue. Fixing at source makes the
configs self-describing and stops the "works for me but won't load
on a fresh node" failure mode.
## 2. bootstrap-l2 normalization is now fail-loud (cmd/bootstrap-l2/main.go)
Pre-flight repair upgrades from silent fix to:
- Normalize 0X → 0x (preserve body case for EIP-55 checksum signal)
- Add missing 0x prefix (repair, log count)
- Reject anything that doesn't match /^0x[0-9a-fA-F]{40}$/ AFTER
repair via log.Fatalf with up to 5 sample keys
Per the user's directive:
> "normalize alloc keys on load/write or fail loudly before chain
> creation. I prefer normalization plus a validation log, not
> silent mutation with no signal."
Log line now shows both healthy and repaired counts:
[devnet] hanzo: alloc keys ok=2 0x-repaired=0 (in-memory only, file unchanged)
A genuinely malformed key (non-hex, wrong length) aborts the entire
bootstrap before any CreateNetworkTx burns LUX:
[devnet] hanzo: 1 malformed alloc keys (require canonical
/^0x[0-9a-fA-F]{40}$/); sample: ["0xZZZ..."]
## 3. Single source of truth: normalizeAllocKey() helper
New pure function returns `(canonical, repaired, valid)`. Every
consumer (the in-memory repair, future validation tooling, any
on-disk rewriter) routes through this one function. Routes:
- body-case PRESERVED (EIP-55 checksum)
- prefix-case NORMALIZED (`0X` → `0x`)
- shape-validated (length 40, hex only)
- idempotent
## Tests
9 new tests in cmd/bootstrap-l2/main_test.go:
- already-canonical roundtrip
- missing-prefix repair flag
- 0X-prefix normalization
- body-case preservation (EIP-55 invariant)
- short body rejection (39/38/empty)
- long body rejection (41)
- non-hex rejection (g, _, space)
- empty string rejection
- idempotency under repeated normalize calls
All pass. Build clean.
dropping aliases on github.com/luxfi/crypto and github.com/luxfi/crypto/secp256k1.
no name collisions with stdlib (validator_keys.go's crypto/rand is rand, not crypto).
also purges remaining ethAddr / luxAddr / luxKey local vars (renamed to addr /
utxoAddr / secpKey).
Renames Go struct field ETHAddr → EVMAddr across types.go, keys.go,
allocations.go, validator_keys.go, config.go, builder/builder.go,
cmd/genesis, cmd/checkkeys. JSON tags stayed evmAddr already (from
v1.12.6). Result: matched pair EVMAddr+UTXOAddr on both Go and JSON
sides.
Kept ParseETHAddress as the input-parsing function name — it describes
what it parses (an Ethereum-format hex address), not the field it
populates (an EVMAddr).
LLM.md updated to state the canonical pair plainly; no rename history.
Renames Allocation.EVMAddr → ETHAddr (json tag `ethAddr`) and
KeyInfo.EVMAddr → ETHAddr across pkg/genesis, builder, cmd/genesis,
cmd/checkkeys. AllocationJSON drops the legacy `evmAddr` tag entirely
— there is no aliasing, no UnmarshalJSON normalizer, no fallback.
JSON files and Go code share one canonical pair: ethAddr + utxoAddr.
configs/getgenesis_test.go updated to assert against the canonical
field. parseAllocations error message says "invalid eth address"
to match the new identifier.
Tests:
ok github.com/luxfi/genesis/builder
ok github.com/luxfi/genesis/configs
ok github.com/luxfi/genesis/pkg/genesis
Removed:
- LoadKeysFromMnemonic's `nid uint32` parameter (was ignored)
- BuildWalletAllocations (replaced by canonical BuildBIP44WalletAllocations)
- BuildWalletKeyHex (no callers after removing the nid-hardened scheme)
- deriveLuxAccount helper (m/44'/9000'/nid'/...) — only used by removed code
- genesis_hd_branches_test.go entire file (tested abandoned NID-hardening
requirement; canonical Avalanche/Lux BIP44 is intentionally nid-stable
so the same mnemonic gives the same addresses in every wallet)
TestTransferPChain now skips by default — opts in via LUX_LIVE_TESTS=1
(it hits a live api.lux-dev.network endpoint, not appropriate for unit-test
runs that fail when CF returns 522).
cmd/checkkeys/main.go: dropped unused NETWORK_ID env handling.
cmd/genesis/main.go: BuildWalletAllocations → BuildBIP44WalletAllocations.
All tests pass in clean offline mode now:
configs OK (TestPrimaryChainShards_PerChainCanonicalChainID green)
pkg/genesis OK
builder OK
Address types now reflect the actual two-address model that exists across
all chains:
- EVMAddr (0x H160) — C-Chain and every other EVM chain
- UTXOAddr (bech32, P-/X- prefix interchangeable) — P-Chain and X-Chain
The old names (ETHAddr / LUXAddr / avaxAddr / xAddr) confused two
orthogonal axes (Ethereum-vs-Lux brand × address-encoding-type) with
each other. UTXO/EVM is the actual technical distinction.
Backward compat:
- JSON: AllocationJSON.UnmarshalJSON accepts evmAddr/ethAddr +
utxoAddr/xAddr/luxAddr/avaxAddr (legacy upstream Avalanche).
- Existing config files: bumped with both new + legacy fields present.
Builds + tests green on luxfi/genesis and luxfi/node/genesis/builder.
Also: each allocation gets 50M LUX on X (initialAmount) + 50M LUX on P
(unlockSchedule locktime=0). For 1000 accounts: 50B on each chain →
100B total per env across P+X. mainnet legacy retained at-is (1.08M
historic blocks reference original asset IDs).
Closes the v1.9.14 CI build failure: pkg/genesis/security_profile_test.go
references consensusconfig.StrictPQ()/ProfileStrictPQ which were only
introduced when the Lux prefix was dropped from profile identifiers.
That symbol first ships in consensus v1.23.25.
Also fixes the gofmt drift on builder/builder.go, cmd/genesis/main.go,
and pkg/genesis/keys.go that was failing the Test job.
Renames LUX_-prefixed env vars to canonical, non-noisy forms:
- LUX_MNEMONIC dropped from getMnemonicEnv() lookup chain.
Priority is now MNEMONIC > LIGHT_MNEMONIC.
All callers, doc comments, and test
t.Setenv("LUX_MNEMONIC", "") clears removed.
- LUX_NETWORK_ID -> NETWORK_ID
- LUX_GENESIS_DIR -> GENESIS_DIR
- LUX_KEYS_DIR -> KEYS_DIR (doc fix; code already used KEYS_DIR)
- LUX_BOOTSTRAPPERS_FILE-> BOOTSTRAPPERS_FILE
- LUX_PCHAIN_ALLOCS -> PCHAIN_ALLOCS
- LUX_PCHAIN_ALLOCS_FILE-> PCHAIN_ALLOCS_FILE
Doc string mentions of past-but-still-supported `LUX_X` env var names
("set MNEMONIC or LUX_MNEMONIC env var") are also dropped — the env
var is just MNEMONIC now.
LUX_DISABLE_CCHAIN / LUX_CCHAIN_GENESIS_FILE history left in LLM.md
(historical changelog table) and chain_shards_test.go (comment
explicitly documenting the data-driven replacement contract). These
are documenting past behavior, per task convention for historical
references.
BREAKING: external callers/operators must update env var names. Per
CLAUDE.md no-backwards-compatibility rule, the old forms are removed.
Make every primary-network chain (X/C/D/Q/A/B/T/Z/G/K) opt-in via its
own <x>chain.json shard. Absent shard -> empty ConfigOutput field ->
builder skips the CreateChainTx -> daemon doesn't start that chain.
Lux mainnet/testnet/devnet ship full chain set as shards; localnet and
local ship the same canonical X asset shard. P-only sovereign L1 boot
shape (<tenant> etc.) is achieved by shipping pchain.json+network.json
only — no env knob, no special builder branch.
X-Chain genesis was previously hardcoded inside FromConfig as a literal
{Symbol:"LUX",Name:"Lux",Denomination:9}. Now sourced from xchain.json
shard, which keeps the same canonical asset for Lux but lets a sibling
network (lqd / <tenant>) ship its own descriptor or ship none and
boot a P-only primary network.
builder.FromConfig:
- HRP hoisted above the X-Chain conditional (used on both branches).
- X-Chain construction gated on config.XChainGenesis != "".
- Returns ids.Empty for xAssetID when X is absent.
- Single chainEntries table replaces the per-chain switch and the
DefaultChainGenesis stub-data fallback.
- "platform" alias renamed to "protocol" (PChainAliases / VMAliases /
Aliases output) — value is "P-Chain", served by ProtocolVM.
configs.loadAllChainShards / readAllChainShards:
- Single dispatch over primaryChainShardFiles in canonical order
(X,C,D,Q,A,B,T,Z,G,K). Adding a new primary chain is one entry +
one slot, not a new env knob and not a new builder branch.
pkg/genesis/types.go:
- ConfigOutput.XChainGenesis (omitempty) added; threaded through
Config <-> ConfigOutput conversions.
Tests:
- builder/builder_test.go: TestFromConfig_ChainSetIsShardDriven pins
the contract — full mainnet emits 10 chains + non-empty xAssetID;
same config with all chain shards stripped emits 0 chains + empty
xAssetID, validators + UTXOs intact.
- configs/chain_shards_test.go: TestGetGenesis_XChainShardPresent...
asserts every Lux network embeds the canonical {LUX,Lux,9} asset;
AbsentShardEmptyOptIn extended to cover xChainGenesis.
Mirror the LUX_DISABLE_CCHAIN three-precedence pattern for Q-Chain
(Quantum VM) and Z-Chain (ZK VM). Downstream forks that run only
P+X on the primary network (<tenant> is the first) set
LUX_DISABLE_QCHAIN=1 and LUX_DISABLE_ZCHAIN=1 to omit those entries
from the marshalled primary genesis, and builder.FromConfig's
'if config.QChainGenesis != ""' / 'ZChainGenesis' guards skip the
entries at build time.
Precedence (identical to C-Chain knob):
1. LUX_DISABLE_<X>CHAIN=1 → empty (omit from primary genesis)
2. LUX_<X>CHAIN_GENESIS_FILE=<path> → file contents verbatim
3. unset → DefaultPlaceholderGenesis
Q-Chain is the post-quantum primitives chain; Z-Chain is the
zero-knowledge primitives chain. Both are Lux Network-specific and
have no place in the primary genesis of downstream L1s.
The repo had no .github/workflows/ at all, so tag pushes never produced
release binaries.
Added:
.github/workflows/ci.yml go vet, gofmt -s -d, go test (push/PR to main)
.github/workflows/release.yml build genesis for linux|darwin|windows
amd64/arm64 + sha256, attach to GH release
CI runs `go test -skip TestGetGenesisLocalnet ./...` until the embedded
localnet genesis file is regenerated (expected 5e17 wei / 0.5 LUX, got
5e14). All other tests are green.
While here:
* go.mod bumped Go directive 1.26.1 → 1.26.2 (matches new toolchain
used by ci.yml / release.yml).
* luxfi/metric v1.4.11 → v1.4.12. v1.4.11 had a build-tag bug where
both process_metrics_other.go and process_metrics_windows.go used
//go:build windows, causing duplicate symbols on GOOS=windows. v1.4.12
fixes the tag to //go:build !unix && !windows. Required for the
windows/amd64 leg of the release matrix.
* `gofmt -s -w` on four pre-existing unformatted files so the new
gofmt CI gate is green from day one.
HIP-0077 §"Identity" HD path alignment. Every public derivation entry
point now takes the network id and derives on the per-network
hardened branches mandated by the spec:
m/44'/9000'/nid'/0'/i' -> device_pq_key[i] (ML-DSA-65)
m/44'/9000'/nid'/1'/i' -> device_lux_key[i] (secp256k1)
All five levels (44', 9000', nid', branch, index) are now hardened on
the secp256k1 branch. Per-network hardening (nid' at the account level)
means the same mnemonic on different network ids derives fully
independent keypairs — no cross-network key reuse possible.
ML-DSA-65 keypair: expand the 32-byte BIP-32 child seed via
SHAKE-256("LUX/HIP-0077/mldsa65" || child_seed) into the 32-byte xi
that FIPS 204 §5.1 KeyGen consumes. Domain-separated so future schemes
sharing the same BIP-32 child seed cannot collide. ML-DSA backend:
cloudflare/circl mldsa65.
LoadKeysFromMnemonicEnvForNetwork blacklists 6 known public mnemonics
(BIP-39 abandon vector, Hardhat default, Trezor demo) and refuses to
proceed in production — closes F31. The CI/dev path still works with
LUX_LIGHT_MNEMONIC=1 explicitly set.
cmd/checkkeys + cmd/genesis: thread the network id through every
call site. cmd/derive100: helper that derives the first 100 PQ+secp
keypairs per network id for the genesis allocation.
Tests: pkg/genesis PASS (HD branches + LIGHT_MNEMONIC guard).
CHANGELOG.md added.
Breaking signature change for LoadKeysFromMnemonic /
LoadKeysFromMnemonicEnv / BuildWalletAllocations / BuildWalletKeyHex
(every entry point now takes nid uint32). Patch-bump.
When LUX_MNEMONIC is set and -wallet-keys > 0, derive secp256k1
keys at m/44'/9000'/0'/0/{i} and add free spending allocations
(no vesting, no lock) with both luxAddr and ethAddr.
Flags:
-wallet-keys N Number of mnemonic-derived keys (default 0)
-wallet-amount N LUX per key (default 10000)
These keys can be used by the bootstrap tool to create L1 chains
without needing access to node staker TLS cert keys.