Files
Hanzo AI 4383ad4583 genesis: unify mnemonic env to LUX_MNEMONIC + kill vesting docstring rot
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.
2026-06-02 22:21:27 -07:00

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.MnemonicEnvVar constant exposes the name ("LUX_MNEMONIC").
  • getMnemonicEnv() reads only LUX_MNEMONIC. No fallback.
  • All CLI help, error messages and tests now reference LUX_MNEMONIC.
  • LightMnemonic constant still holds the well-known dev seed value: pass it as the value of LUX_MNEMONIC to 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, VestingPeriods constants
  • buildUnlockSchedule() function (pkg/genesis/keys.go)
  • VestingConfig struct + DefaultVesting() + WithVesting()
  • vesting branches in ChainAllocations.PChain() / PChainMap()
  • GeneratePChainAllocationsWithVesting() function
  • MainnetAllocations no longer calls WithVesting(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 (BuildWalletKeyHex on the old code) and load them via KEYS_DIR instead of LUX_MNEMONICLoadKeysFromDir is 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.