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

114 lines
5.2 KiB
Markdown

# 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_MNEMONIC``LoadKeysFromDir` 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.