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.
5.2 KiB
genesis CHANGELOG
Unreleased
Mnemonic env unification — one var, one way
The genesis mnemonic is now read from exactly one env var: LUX_MNEMONIC.
The previous two-env fallback chain (MNEMONIC then LIGHT_MNEMONIC)
is gone — there is one and only one canonical name.
pkg/genesis.MnemonicEnvVarconstant exposes the name ("LUX_MNEMONIC").getMnemonicEnv()reads onlyLUX_MNEMONIC. No fallback.- All CLI help, error messages and tests now reference
LUX_MNEMONIC. LightMnemonicconstant still holds the well-known dev seed value: pass it as the value ofLUX_MNEMONICto bootstrap a local network.
Per-env isolation comes from a DIFFERENT mnemonic per env (loaded from KMS for production), not from the env-var name.
Dead-code removal — vesting rot
The "50M free + 50M vesting at 1%/year over 100y from Jan 1 2020"
docstring was a lie: the canonical builder buildConfigFromKeyInfos
never wired the 100-period schedule (the prior code admits the schedule
"overflows zapdb batch limit"). Drop the unreachable plumbing:
StakingStartTime,UnlockInterval,VestingPeriodsconstantsbuildUnlockSchedule()function (pkg/genesis/keys.go)VestingConfigstruct +DefaultVesting()+WithVesting()- vesting branches in
ChainAllocations.PChain()/PChainMap() GeneratePChainAllocationsWithVesting()functionMainnetAllocationsno longer callsWithVesting(DefaultVesting())
The validator stake-lock (3-entry 5y/10y/20y schedule attached to the
validator's UTXO inside buildConfigFromKeyInfos) is unchanged — that
locked-stake bucket is the only UnlockSchedule in the canonical
genesis path and the ProtocolVM needs it.
pkg/genesis/networks.go adds the canonical primary-network-id table
(MainnetID 1, TestnetID 2, DevnetID 3, LocalID 1337) as the
single source of truth for "what envs exist".
Breaking — HIP-0077 §"Identity" HD path alignment
pkg/genesis/keys.go now derives keys on the per-network hardened
branches mandated by HIP-0077:
| derived | new path | curve / scheme | old path (removed) |
|---|---|---|---|
device_pq_key[i] |
m/44'/9000'/nid'/0'/i' |
ML-DSA-65 | n/a (was loaded from disk only) |
device_lux_key[i] |
m/44'/9000'/nid'/1'/i' |
secp256k1 | m/44'/9000'/0'/0/i |
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.
The ML-DSA-65 keypair is generated by expanding the 32-byte BIP-32
child seed via SHAKE-256("LUX/HIP-0077/mldsa65" || child_seed) into
the 32-byte ξ that FIPS 204 §5.1 KeyGen consumes. The expansion is
domain-separated so future schemes sharing the same BIP-32 child seed
cannot collide.
Function signature changes
// before
LoadKeysFromMnemonic(mnemonic string, numAccounts int) ([]KeyInfo, error)
LoadKeysFromMnemonicEnv(numAccounts int) ([]KeyInfo, error)
BuildWalletAllocations(numKeys int, amountPerKey uint64) ([]Allocation, error)
BuildWalletKeyHex(index int) (string, error)
// after — every public derivation entry point now takes the network id
LoadKeysFromMnemonic(mnemonic string, nid uint32, numAccounts int) ([]KeyInfo, error)
LoadKeysFromMnemonicEnv(nid uint32, numAccounts int) ([]KeyInfo, error)
BuildWalletAllocations(nid uint32, numKeys int, amountPerKey uint64) ([]Allocation, error)
BuildWalletKeyHex(nid uint32, index int) (string, error)
LoadKeysFromMnemonicEnvForNetwork(networkID, numAccounts) keeps its
signature; it now passes networkID through as the nid for the
underlying derivation, preserving the F30 public-mnemonic guard.
KeyInfo.MLDSAPublicKey is now populated from the deterministic
mnemonic derivation. Old loads from nodeDir/mldsa/public.key still
work for keys-on-disk paths (LoadKeysFromDir).
Migration
Any address derived under the old m/44'/9000'/0'/0/i path will NOT
reproduce under the new layout. Specifically:
- Treasury and any other addresses pinned via the mnemonic (validator ETH/P-chain addresses, fee reserve keys, wallet allocations) must be re-derived after upgrading.
- If you need to keep the old addresses spendable, export their private
keys (
BuildWalletKeyHexon the old code) and load them viaKEYS_DIRinstead ofLUX_MNEMONIC—LoadKeysFromDiris unchanged.
Per CLAUDE.md no backwards compatibility, only forwards perfection:
this is the correct break; clients deriving from the HIP-0077 spec
agree with luxd going forward, and nobody is left guessing which
historical path to use.
Library note
ML-DSA-65 keygen currently uses github.com/cloudflare/circl/sign/mldsa/mldsa65
because github.com/luxfi/crypto/pq/mldsa (v1.17.44) does not yet
expose a public NewKeyFromSeed entry point. Migration is tracked by
a TODO(canonical) at the import site in pkg/genesis/keys.go; the
canonical replacement MUST adopt the same
SHAKE-256("LUX/HIP-0077/mldsa65" || child_seed) expansion to preserve
determinism.