luxfi/genesis only owns Lux primary-network genesis (mainnet/testnet/
devnet/localnet). Brand L1 genesis blobs now live next to the brand
that owns them, in each brand's universe repo:
hanzo: hanzoai/universe → configs/genesis/{mainnet,testnet,devnet}/
zoo: zooai/universe → configs/genesis/{mainnet,testnet,devnet}/
The pars-* and spc-* dirs have no separate universe repo yet — they
cohabitate this repo under configs/_orphan-l2/ until one is spun up,
explicitly marked as orphans (see configs/_orphan-l2/README.md).
This commit:
- removes configs/hanzo-{mainnet,testnet,devnet}/genesis.json
and configs/zoo-{mainnet,testnet,devnet}/genesis.{json,upgrade.json}
- leaves MOVED.txt tombstones at each old path pointing at the
new canonical home
- moves configs/{pars,spc}-{mainnet,testnet,devnet}/ to
configs/_orphan-l2/ (git mv — history preserved)
- adds pkg/genesis/no_brand_shadow_test.go which fails if any
genesis.json reappears under configs/hanzo-* / zoo-* / pars-* /
spc-* at the top level
- updates LLM.md + configs/mainnet/UPGRADE_NOTES.md doc refs to
point at the new locations
Zero Go code references the moved paths — configs/configs.go
//go:embed only covers Lux primary (mainnet testnet devnet localnet),
not brand dirs. Verified by full pkg/genesis test pass.
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.
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.
Go field stays ETHAddr (names the H160 byte format, which IS Ethereum's
address spec — we're wire-compatible, not branded). JSON tag changes to
evmAddr so the user-facing pair (evmAddr / utxoAddr) describes our chain
models symmetrically without branding us as Ethereum.
This is the hybrid that fell out of the linter pass: Go identifier =
format-name, JSON tag = chain-model-name. Documented in LLM.md so we
don't regress.
Touches: pkg/genesis/{types,keys,allocations,validator_keys,config}.go,
configs/*/genesis.json + pchain.json + getgenesis_test.go,
scripts/gen-localnet.py. Tests green.
Live deployed hashes verified byte-equal to current canonical cchain.json:
testnet: 0xfc909f7e992d9cb91485f114f6d333f3823a12f2c72bbf51ed2c8eea749b2d2e
devnet: 0x836f6053473e4331bb347afc45b641f12075c63a302f4e56e64239a3ba4acd4b
Previous values were from before commits ae1c52b (Shanghai/Cancun/Prague
at genesis) and 88b6561 (gasLimit 1B). testnet+devnet were both "block 0
fresh" with no committed historical state, so the hash update is in-spec.
Mainnet hash unchanged — its cChainGenesis is locked by the 1.08M-block
historical chain.
Every primary network now ships all 10 chain shards (X, C, D, Q, A, B, T, Z, G, K).
Each chain spawns at startup with zero post-launch CreateBlockchainTx — chain set
is fully data-driven by which shards the operator ships.
Per-letter EVM chainIds locked at 96369 + 100*Δ_from_C (mainnet base) with the
familiar testnet=-1 / devnet=+1 triple inherited from C-Chain:
C 96369/96368/96370 D 96469/96468/96470 Q 96569/96568/96570
A 96669/96668/96670 B 96769/96768/96770 T 96869/96868/96870
Z 96969/96968/96970 G 97069/97068/97070 K 97169/97168/97170
Localnet uses a parallel 31337 + 110*Δ series.
F-Chain ≡ T-Chain (threshold FHE) and M-Chain ≡ K-Chain (KMS-MPC) wired in via
builder alias lists — letter taxonomy decomplected from canonical VM IDs.
Each shard carries:
- chainId / networkId / timestamp (chain identity at boot)
- vm + description (operator transparency)
- per-VM seed extras (DEX fee config, T-Chain DKG params, B-Chain signer set,
Q-Chain pq scheme pin, K-Chain MPC topology — all empty/default initial state)
Contract locked by configs.TestGetGenesis_AllPrimaryChainsBakedIn — every primary
network must ship all 10 letter-chain shards. Removing a chain on any network is
now a load-bearing test edit, not a silent operator switch.
Mainnet C-Chain invariants preserved: chainId=96369, genesisHash unchanged,
stateRoot=0x2d1cedac…, warp precompile in alloc, warpConfig.blockTimestamp=
1730446786, treasury 2T LUX.
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.
Lux EVM activation is gated by a single canonical chain config field:
EVMTimestamp / json:'evmTimestamp'
The legacy 'SubnetEVMTimestamp' / 'subnetEVMTimestamp' field was a duplicate
alias for the same activation. Removed:
- geth/params/config.go: ChainConfig.SubnetEVMTimestamp field
- geth/params/config.go: IsEVM() now checks EVMTimestamp only
- geth/params/config.go: isLuxL2Chain check uses EVMTimestamp only
- all genesis JSON configs (mainnet/testnet/devnet × all chains)
- all helm chart genesis files (charts/lux/genesis/*.json)
- all docs (LLM.md, *.mdx)
One field, one way.