From 4383ad45831a66efa0e81a3628e8bd5b5de6fe34 Mon Sep 17 00:00:00 2001 From: Hanzo AI Date: Tue, 2 Jun 2026 22:21:27 -0700 Subject: [PATCH] genesis: unify mnemonic env to LUX_MNEMONIC + kill vesting docstring rot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 40 +++++- LLM.md | 4 +- cmd/checkkeys/main.go | 10 +- cmd/derive100/main.go | 2 +- cmd/genesis/main.go | 12 +- configs/getgenesis_test.go | 2 +- pkg/genesis/allocations.go | 94 +++----------- pkg/genesis/keys.go | 159 ++++++++++------------- pkg/genesis/light_mnemonic_guard_test.go | 43 +++--- pkg/genesis/networks.go | 45 +++++++ pkg/genesis/validator_keys.go | 41 ------ 11 files changed, 201 insertions(+), 251 deletions(-) create mode 100644 pkg/genesis/networks.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 025fdae..1b5b619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,44 @@ ## 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 @@ -57,7 +95,7 @@ reproduce under the new layout. Specifically: 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 `MNEMONIC` — `LoadKeysFromDir` is unchanged. + `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 diff --git a/LLM.md b/LLM.md index 1b268e6..c322313 100644 --- a/LLM.md +++ b/LLM.md @@ -17,7 +17,7 @@ silent classification change. |-----|-----|--------| | `85a8fc8` | v1.9.6 | Pin ChainSecurityProfile into Config + Resolve at load (closes F102 genesis layer) | | `1d73c8d` | v1.9.6 | Swap mldsa65 keygen from cloudflare/circl to luxfi/crypto/pq/mldsa | -| `e698307` | v1.9.5 | HD branch hardening + LIGHT_MNEMONIC production refusal (F12/F30/F31/F35) | +| `e698307` | v1.9.5 | HD branch hardening + LightMnemonic production refusal (F12/F30/F31/F35) | | `fe4781c` | v1.9.5 | `LUX_DISABLE_CCHAIN=1` omits C-Chain from primary genesis | | `2b16f45` | v1.9.5 | Operator-overridable C-Chain genesis via `LUX_CCHAIN_GENESIS_FILE` | @@ -350,7 +350,7 @@ Generate genesis configurations: ```bash # Generate from mnemonic -MNEMONIC="test test..." ./genesis -network-id 96369 -validators 5 -output genesis.json +LUX_MNEMONIC="test test..." ./genesis -network-id 96369 -validators 5 -output genesis.json # From existing keys ./genesis -network mainnet -keys-dir ~/.lux/keys -output genesis.json diff --git a/cmd/checkkeys/main.go b/cmd/checkkeys/main.go index fe47170..cb59805 100644 --- a/cmd/checkkeys/main.go +++ b/cmd/checkkeys/main.go @@ -8,15 +8,9 @@ import ( ) func main() { - mnemonic := "" - for _, env := range []string{"MNEMONIC", "LIGHT_MNEMONIC"} { - if v := os.Getenv(env); v != "" { - mnemonic = v - break - } - } + mnemonic := os.Getenv(genesis.MnemonicEnvVar) if mnemonic == "" { - fmt.Println("mnemonic not set (set MNEMONIC or LIGHT_MNEMONIC)") + fmt.Printf("mnemonic not set (set %s)\n", genesis.MnemonicEnvVar) os.Exit(1) } diff --git a/cmd/derive100/main.go b/cmd/derive100/main.go index 5c262d0..c7a6df2 100644 --- a/cmd/derive100/main.go +++ b/cmd/derive100/main.go @@ -129,7 +129,7 @@ func main() { fmt.Fprintln(os.Stderr, "usage: derive100 ") fmt.Fprintln(os.Stderr, " hrp: test|lux|dev|local") fmt.Fprintln(os.Stderr, " rpc: http://24.199.71.151:9650") - fmt.Fprintln(os.Stderr, " mnemonic-env: MNEMONIC|LIGHT_MNEMONIC") + fmt.Fprintln(os.Stderr, " mnemonic-env: LUX_MNEMONIC") os.Exit(1) } hrp := os.Args[1] diff --git a/cmd/genesis/main.go b/cmd/genesis/main.go index a5ad0a0..231bd98 100644 --- a/cmd/genesis/main.go +++ b/cmd/genesis/main.go @@ -42,7 +42,7 @@ func main() { output = flag.String("output", "", "Output file path (default: stdout)") allocation = flag.Uint64("allocation", genesis.DefaultAllocationPerValidator, "Allocation per validator in nLUX") validators = flag.Int("validators", 3, "Number of validators for mnemonic-based genesis") - walletKeys = flag.Int("wallet-keys", 0, "Number of mnemonic-derived wallet keys to fund (Lux-internal hardened path m/44'/9000'/nid'/1'/i'; requires MNEMONIC env)") + walletKeys = flag.Int("wallet-keys", 0, "Number of mnemonic-derived wallet keys to fund (Lux-internal hardened path m/44'/9000'/nid'/1'/i'; requires LUX_MNEMONIC env)") walletAmount = flag.Uint64("wallet-amount", 10000, "Allocation per wallet key in LUX (default: 10000)") bip44WalletKeys = flag.Int("bip44-wallet-keys", 0, "Number of canonical BIP44 wallet keys to fund (m/44'/9000'/0'/0/i — SLIP-0044 coin type 9000)") cchainPath = flag.String("cchain", "", "Path to existing C-Chain genesis (preserves original)") @@ -115,10 +115,10 @@ Flags: Environment Variables: KEYS_DIR Directory containing node keys - MNEMONIC BIP39 mnemonic for key derivation + LUX_MNEMONIC BIP39 mnemonic for key derivation PRIVATE_KEY Single private key (hex) - GENESIS_DIR Directory containing genesis component files - NETWORK_ID Network ID + GENESIS_DIR Directory containing genesis component files + NETWORK_ID Network ID Examples: # Generate local genesis from keys in ~/.lux/keys @@ -128,7 +128,7 @@ Examples: genesis -network mainnet -cchain mainnet/cchain.json -output primary.json # Generate from mnemonic with 3 validators - MNEMONIC="your mnemonic here" genesis -validators 3 -output primary.json + LUX_MNEMONIC="your mnemonic here" genesis -validators 3 -output primary.json # Load from existing genesis directory genesis -genesis-dir ./mainnet -output primary.json`) @@ -189,7 +189,7 @@ func buildConfig(networkID uint32, keysDir, genesisDir string, allocation uint64 config, err = genesis.BuildConfigFromKeys(networkID, keysDir, allocation) if err != nil { - return nil, fmt.Errorf("failed to build config: %w (try setting KEYS_DIR, MNEMONIC, or PRIVATE_KEY)", err) + return nil, fmt.Errorf("failed to build config: %w (try setting KEYS_DIR, LUX_MNEMONIC, or PRIVATE_KEY)", err) } config = maybeAddWalletAllocations(config, walletKeys, walletAmount) diff --git a/configs/getgenesis_test.go b/configs/getgenesis_test.go index 2726cee..7e126b4 100644 --- a/configs/getgenesis_test.go +++ b/configs/getgenesis_test.go @@ -111,7 +111,7 @@ func TestGetGenesisLocalnet(t *testing.T) { } // Verify first allocation is canonical BIP44 m/44'/9000'/0'/0/0 - // for LIGHT_MNEMONIC. ETH addr is the keccak256(pubkey) projection + // for LightMnemonic. ETH addr is the keccak256(pubkey) projection // of the same secp256k1 spending key, computed from the BIP44 child // at index 0 (not the Lux-internal hardened path). first := allocs[0].(map[string]interface{}) diff --git a/pkg/genesis/allocations.go b/pkg/genesis/allocations.go index abf66db..7c19707 100644 --- a/pkg/genesis/allocations.go +++ b/pkg/genesis/allocations.go @@ -9,36 +9,19 @@ import ( ) // ChainAllocations holds genesis allocations for all chains. -// Use NewAllocations() or AllocationBuilder to create. +// Use NewAllocations() to create. +// +// All allocations are immediate (no vesting). The long-locked validator +// stake bucket is attached separately by buildConfigFromKeyInfos and is +// the only place an UnlockSchedule appears in the canonical genesis path. type ChainAllocations struct { keys []ValidatorKeyInfo hrp string amount uint64 - - // Vesting configuration (optional) - vesting *VestingConfig -} - -// VestingConfig defines the vesting schedule parameters. -type VestingConfig struct { - StartTime uint64 // Unix timestamp when vesting starts - Interval uint64 // Seconds between each unlock - Periods int // Number of unlock periods -} - -// DefaultVesting returns the standard vesting config: -// - Start: Jan 1, 2020 -// - 1% unlocks per year for 100 years -func DefaultVesting() *VestingConfig { - return &VestingConfig{ - StartTime: StakingStartTime, // Jan 1, 2020 - Interval: UnlockInterval, // 1 year - Periods: 100, // 100 periods = 1% per year - } } // NewAllocations creates a ChainAllocations for the given validator keys. -// Default amount is 100M LUX per key, no vesting. +// Default amount is DefaultValidatorAllocation per key, immediate spend. func NewAllocations(keys []ValidatorKeyInfo, hrp string) *ChainAllocations { return &ChainAllocations{ keys: keys, @@ -53,14 +36,8 @@ func (a *ChainAllocations) WithAmount(amount uint64) *ChainAllocations { return a } -// WithVesting enables vesting with the specified config. -// Use DefaultVesting() for standard 100-year vesting from Jan 1 2020. -func (a *ChainAllocations) WithVesting(config *VestingConfig) *ChainAllocations { - a.vesting = config - return a -} - // PChain returns P-chain allocations in the standard AllocationJSON format. +// All entries are immediately spendable (locktime=0). func (a *ChainAllocations) PChain() ([]AllocationJSON, error) { allocations := make([]AllocationJSON, len(a.keys)) @@ -70,25 +47,11 @@ func (a *ChainAllocations) PChain() ([]AllocationJSON, error) { return nil, fmt.Errorf("failed to format P-chain address for key %d: %w", i, err) } - var unlockSchedule []LockedAmount - var initialAmount uint64 - - if a.vesting != nil { - // Vested allocation - unlockSchedule = buildUnlockSchedule(a.amount, a.vesting.StartTime, a.vesting.Interval, a.vesting.Periods) - initialAmount = a.amount // X-chain gets initial amount with vesting - } else { - // Immediate allocation (no vesting) - // X-chain gets full initial amount for chain creation operations - unlockSchedule = []LockedAmount{{Amount: a.amount, Locktime: 0}} - initialAmount = a.amount - } - allocations[i] = AllocationJSON{ EVMAddr: key.EVMAddr, UTXOAddr: utxoAddr, - InitialAmount: initialAmount, - UnlockSchedule: unlockSchedule, + InitialAmount: a.amount, + UnlockSchedule: []LockedAmount{{Amount: a.amount, Locktime: 0}}, } } return allocations, nil @@ -130,6 +93,7 @@ func (a *ChainAllocations) CChainMap() map[string]map[string]string { } // PChainMap returns P-chain allocations as interface maps (for netrunner compatibility). +// All entries are immediately spendable (locktime=0). func (a *ChainAllocations) PChainMap() ([]map[string]interface{}, error) { allocations := make([]map[string]interface{}, len(a.keys)) @@ -139,34 +103,13 @@ func (a *ChainAllocations) PChainMap() ([]map[string]interface{}, error) { return nil, fmt.Errorf("failed to format P-chain address for key %d: %w", i, err) } - var unlockSchedule []map[string]interface{} - var initialAmount uint64 - - if a.vesting != nil { - // Build vesting schedule as maps - locked := buildUnlockSchedule(a.amount, a.vesting.StartTime, a.vesting.Interval, a.vesting.Periods) - unlockSchedule = make([]map[string]interface{}, len(locked)) - for j, l := range locked { - unlockSchedule[j] = map[string]interface{}{ - "amount": l.Amount, - "locktime": l.Locktime, - } - } - initialAmount = a.amount - } else { - // Immediate allocation (no vesting) - // X-chain gets full initial amount for chain creation operations - unlockSchedule = []map[string]interface{}{ - {"amount": a.amount, "locktime": uint64(0)}, - } - initialAmount = a.amount - } - allocations[i] = map[string]interface{}{ - "evmAddr": key.EVMAddr, - "utxoAddr": utxoAddr, - "initialAmount": initialAmount, - "unlockSchedule": unlockSchedule, + "evmAddr": key.EVMAddr, + "utxoAddr": utxoAddr, + "initialAmount": a.amount, + "unlockSchedule": []map[string]interface{}{ + {"amount": a.amount, "locktime": uint64(0)}, + }, } } return allocations, nil @@ -206,15 +149,14 @@ func QuickAllocations(keys []ValidatorKeyInfo, hrp string, amount uint64) (*AllC return NewAllocations(keys, hrp).WithAmount(amount).All() } -// MainnetAllocations creates allocations with standard mainnet vesting (1B LUX, 100 years). +// MainnetAllocations creates immediate-spend mainnet allocations (1B LUX per key). func MainnetAllocations(keys []ValidatorKeyInfo, hrp string) (*AllChainAllocations, error) { return NewAllocations(keys, hrp). WithAmount(OneBillionLUX). - WithVesting(DefaultVesting()). All() } -// TestnetAllocations creates allocations suitable for testnet (100M LUX, no vesting). +// TestnetAllocations creates immediate-spend testnet allocations (100M LUX per key). func TestnetAllocations(keys []ValidatorKeyInfo, hrp string) (*AllChainAllocations, error) { return NewAllocations(keys, hrp). WithAmount(OneHundredMillionLUX). diff --git a/pkg/genesis/keys.go b/pkg/genesis/keys.go index 4d47981..2c78aa8 100644 --- a/pkg/genesis/keys.go +++ b/pkg/genesis/keys.go @@ -34,53 +34,46 @@ import ( ) const ( - // DefaultAllocationPerAccount is 50M LUX per account per chain (P and X). - // 1000 accounts × 50M × 2 chains = 100B LUX of UTXOs in genesis (well - // under the 2T LUX SupplyCap on every network). - // X-Chain: 50M free (immediately spendable from genesis). - // P-Chain: 50M free at genesis (no vesting); the long-tail unlock - // schedule below adds a separate 50M per account that vests 1%/year - // over 100 years from Jan 1 2020 — i.e. each address sees 50M - // spendable on X, 50M spendable on P + 50M vesting on P. + // DefaultAllocationPerAccount is the canonical fresh-genesis amount: + // 50M LUX per derived account, spendable immediately on both X-Chain + // and P-Chain (same address — bech32(ripemd160(sha256(secp256k1_pubkey)))). + // + // 1000 accounts × 50M × 2 chains = 100B LUX of UTXOs in genesis. + // Per-chain SupplyCap is 2T LUX, so each chain individually clears. // // Address scheme — Bitcoin-UTXO-style (more quantum-resistant): // P-Chain / X-Chain addresses are bech32(ripemd160(sha256(pubkey))). - // The public key is *hidden behind two hash layers* until the - // address is first spent. Until first spend, even a future - // quantum adversary cannot recover the private key — Shor's - // algorithm works against the secp256k1 pubkey, but not against - // sha256+ripemd160. Contrast with C-Chain (Ethereum) addresses, - // which are keccakBytes(pubkey)[12:] and effectively expose the - // pubkey on every signature (recovery in ECDSA). Long-term holds - // should sit on P/X, not C, for this reason. + // The public key is hidden behind two hash layers until first spend. + // A future quantum adversary cannot recover the private key from the + // address alone — Shor's works on the pubkey, not on sha256+ripemd160. + // C-Chain (Ethereum) addresses expose the pubkey via ECDSA recovery + // on every signed tx; long-term holds belong on P/X, not C. DefaultAllocationPerAccount = 50_000_000 * Lux // DefaultAllocationPerValidator is kept for backward compatibility DefaultAllocationPerValidator = DefaultAllocationPerAccount - // DefaultNumAccounts is the default number of mnemonic-derived accounts - // Funds 1000 wallet keys at canonical BIP44 m/44'/9000'/0'/0/i so that - // any wallet that derives at this path against the SAME mnemonic sees - // a fundable address on both P and X. + // DefaultNumAccounts is the default number of mnemonic-derived accounts. + // Funds 1000 BIP44 wallet keys at m/44'/9000'/0'/0/i so that any + // canonical-BIP44 wallet (Core, MetaMask, AvalancheJS, `lux key derive`) + // sees a fundable address on both P and X under the same mnemonic. + // Per-network isolation comes from a DIFFERENT mnemonic per env + // (loaded from KMS), not from path divergence. DefaultNumAccounts = 1000 - // StakingStartTime is Jan 1, 2020 00:00:00 UTC - StakingStartTime = 1577836800 - - // UnlockInterval is 1 year in seconds - UnlockInterval = 365 * 24 * 60 * 60 - - // TreasuryAddress is the C-Chain treasury with 2T LUX + // TreasuryAddress is the C-Chain treasury (no per-account C-Chain alloc). TreasuryAddress = "0x9011E888251AB053B7bD1cdB598Db4f9DEd94714" - // TreasuryAmount is 2 trillion LUX in microLUX (2T * 10^6) + // TreasuryAmount is 2 trillion LUX in microLUX (2T * 10^6). TreasuryAmount = 2_000_000_000_000 * Lux - // PChainFeeReserve is 10,000 LUX per validator for P-Chain fees + // PChainFeeReserve is 10,000 LUX per validator earmarked for P-Chain fees. PChainFeeReserve = 10_000 * Lux - // LightMnemonic is the well-known dev mnemonic for local networks (network-id >= 1337). - // NEVER use on public networks (mainnet, testnet, devnet). + // LightMnemonic is the well-known public dev seed. Pass this as the + // value of LUX_MNEMONIC to bootstrap a local network (network ID >= + // 1337). RefuseLightMnemonicOnProduction enforces it cannot be used + // on mainnet/testnet/devnet (network IDs 1/2/3). LightMnemonic = "light light light light light light light light light light light energy" ) @@ -312,21 +305,6 @@ func deriveFeeKey(keysDir string, validatorKey KeyInfo, index int) (*KeyInfo, er }, nil } -// buildUnlockSchedule creates a vesting schedule -func buildUnlockSchedule(totalAmount uint64, startTime uint64, interval uint64, periods int) []LockedAmount { - amountPerPeriod := totalAmount / uint64(periods) - schedule := make([]LockedAmount, 0, periods) - - for i := 0; i < periods; i++ { - schedule = append(schedule, LockedAmount{ - Amount: amountPerPeriod, - Locktime: startTime + uint64(i)*interval, - }) - } - - return schedule -} - // buildCChainGenesisTreasury creates C-chain genesis JSON with only the treasury allocation. // Treasury: 0x9011E888251AB053B7bD1cdB598Db4f9DEd94714 gets 2T LUX. // No mnemonic-derived account allocations on C-Chain. @@ -539,14 +517,14 @@ func mldsaKeygenFromChildSeed(childSeed []byte) ([]byte, error) { return pk.Bytes(), nil } -// LoadKeysFromMnemonicEnv loads keys from mnemonic env vars (MNEMONIC > LIGHT_MNEMONIC). +// LoadKeysFromMnemonicEnv loads keys from the LUX_MNEMONIC env var. // Callers that already know the network id MUST use // LoadKeysFromMnemonicEnvForNetwork instead — it adds the production-safe // public-mnemonic guard around this entry point. func LoadKeysFromMnemonicEnv(numAccounts int) ([]KeyInfo, error) { mnemonic := getMnemonicEnv() if mnemonic == "" { - return nil, fmt.Errorf("mnemonic not set (set MNEMONIC or LIGHT_MNEMONIC)") + return nil, fmt.Errorf("mnemonic not set (set %s)", MnemonicEnvVar) } return LoadKeysFromMnemonic(mnemonic, numAccounts) } @@ -630,28 +608,24 @@ func genesisMessage(networkID uint32) string { } } -// getMnemonicEnv returns the mnemonic from environment variables. -// Priority: MNEMONIC > LIGHT_MNEMONIC -// -// LIGHT_MNEMONIC is the publicly-known dev seed. Anyone in the world can -// derive its first 200 child keys; the auto-fund pre-allocation in -// HIP-0077 §"Auto-funding the first 200 devices" is *only* meant for -// network IDs >= 1337 (dev / primary local mesh). Production networks -// MUST set MNEMONIC. -// -// Use IsLightMnemonic and RefuseLightMnemonicOnProduction to enforce that -// rule wherever a key is loaded for a known network ID. +// MnemonicEnvVar is the single canonical env var name for the genesis +// mnemonic. One env, one way: +// - mainnet/testnet/devnet (IDs 1/2/3): a private hardware-RNG mnemonic +// loaded from KMS; RefuseLightMnemonicOnProduction rejects all known +// public mnemonics so the production guard is enforced at derivation. +// - local (ID 1337): typically set to LightMnemonic for fast dev boot; +// the same public-mnemonic blacklist still applies, but local IDs +// are exempt by IsProductionNetwork. +const MnemonicEnvVar = "LUX_MNEMONIC" + +// getMnemonicEnv returns the mnemonic from the canonical LUX_MNEMONIC env +// var. No fallback chain — one and only one env name. func getMnemonicEnv() string { - for _, env := range []string{"MNEMONIC", "LIGHT_MNEMONIC"} { - if v := os.Getenv(env); v != "" { - return v - } - } - return "" + return os.Getenv(MnemonicEnvVar) } // IsLightMnemonic reports whether the given mnemonic is exactly the -// well-known LIGHT_MNEMONIC dev seed. Compared in constant time so a +// well-known LightMnemonic dev seed value. Compared in constant time so a // timing attacker can't probe the running config from outside. func IsLightMnemonic(mnemonic string) bool { return subtleConstantTimeEqual([]byte(mnemonic), []byte(LightMnemonic)) @@ -660,7 +634,7 @@ func IsLightMnemonic(mnemonic string) bool { // knownPublicMnemonics is the curated set of seeds that anyone in the // world can derive from. Production deployments MUST refuse all of them. // -// LIGHT_MNEMONIC is one row; the rest are the most-frequently-cited public +// LightMnemonic is one row; the rest are the most-frequently-cited public // mnemonics from BIP-39 test vectors, common dev tooling defaults, and // hardware-wallet demo seeds. Any of these on a production network → // every derived child key is publicly enumerable. @@ -687,7 +661,7 @@ var knownPublicMnemonics = []string{ } // IsKnownPublicMnemonic reports whether the given mnemonic appears in any -// well-known public list (LIGHT_MNEMONIC, BIP-39 test vectors, Hardhat +// well-known public list (LightMnemonic, BIP-39 test vectors, Hardhat // default, hardware-wallet demos, etc.). Compared in constant time per // entry so a timing attacker can't probe which entry matched. // @@ -710,15 +684,16 @@ func IsKnownPublicMnemonic(mnemonic string) bool { // IsProductionNetwork reports whether the given numeric network ID is on // the list of *production* Lux networks. Local / primary-local meshes // (network IDs >= 1337, including constants.LocalID = 1337) deliberately -// allow LIGHT_MNEMONIC; mainnet, testnet and any other reserved low-ID -// network refuse it. +// allow public mnemonics like LightMnemonic; mainnet, testnet and any +// other reserved low-ID network refuse them. // -// Network ID convention (mirrors lux/constants): -// - 1 mainnet (production) -// - 2 testnet (production-grade staging — refuses LIGHT_MNEMONIC) -// - 1337 LocalID (free-form local dev, allows LIGHT_MNEMONIC) -// - >= 1337 any tenant local / dev mesh (allows LIGHT_MNEMONIC) -// - 3..1336 reserved; treated as production by default +// Canonical network ID map (mirrors lux/constants): +// - 1 mainnet (production — refuses public mnemonics) +// - 2 testnet (production-grade staging — refuses public mnemonics) +// - 3 devnet (production-grade dev mesh — refuses public mnemonics) +// - 1337 LocalID (free-form local dev — allows LightMnemonic) +// - >= 1337 any tenant local / dev mesh (allows LightMnemonic) +// - 4..1336 reserved; treated as production by default func IsProductionNetwork(networkID uint32) bool { switch networkID { case constants.MainnetID, constants.TestnetID: @@ -730,7 +705,7 @@ func IsProductionNetwork(networkID uint32) bool { } // RefuseLightMnemonicOnProduction returns a non-nil error iff the running -// process is configured with any publicly-known mnemonic (LIGHT_MNEMONIC, +// process is configured with any publicly-known mnemonic (LightMnemonic, // BIP-39 test vectors, Hardhat / Trezor demos, …) AND the supplied // networkID is a production network. Runtime guard required by HIP-0077 // §"Mnemonic exposure" / "Auto-funded blast radius". @@ -743,9 +718,9 @@ func IsProductionNetwork(networkID uint32) bool { // derivation point, never silently produces public-mnemonic-derived // signing keys. // -// The guard widens beyond LIGHT_MNEMONIC to cover the broader -// public-mnemonic blacklist (HIP-0077 red-review F31): BIP-39 abandon -// vector, Hardhat default, Trezor demo, etc. See knownPublicMnemonics. +// The guard covers the full public-mnemonic blacklist (HIP-0077 red-review +// F31): LightMnemonic, BIP-39 abandon vector, Hardhat default, Trezor +// demo, etc. See knownPublicMnemonics. func RefuseLightMnemonicOnProduction(networkID uint32) error { mnemonic := getMnemonicEnv() if mnemonic == "" { @@ -761,11 +736,11 @@ func RefuseLightMnemonicOnProduction(networkID uint32) error { } return fmt.Errorf( "refusing to derive keys: a publicly-known mnemonic is set on production "+ - "network %d (mainnet/testnet/<1337). Public mnemonics (LIGHT_MNEMONIC, "+ + "network %d (mainnet/testnet/<1337). Public mnemonics (LightMnemonic, "+ "BIP-39 test vectors, Hardhat/Trezor demos) are deterministic — anyone "+ - "can derive every child key. Set MNEMONIC env var with "+ - "a private hardware-RNG mnemonic loaded from KMS, or run on a dev "+ - "network ID (>= 1337)", networkID, + "can derive every child key. Set %s with a private hardware-RNG "+ + "mnemonic loaded from KMS, or run on a dev network ID (>= 1337)", + networkID, MnemonicEnvVar, ) } @@ -776,7 +751,7 @@ func RefuseLightMnemonicOnProduction(networkID uint32) error { // // Closes HIP-0077 red-review F30 (the prior LoadKeysFromMnemonicEnv was // guard-free and operators could silently derive on production from -// LIGHT_MNEMONIC). +// the public LightMnemonic). func LoadKeysFromMnemonicEnvForNetwork(networkID uint32, numAccounts int) ([]KeyInfo, error) { if err := RefuseLightMnemonicOnProduction(networkID); err != nil { return nil, err @@ -868,7 +843,7 @@ func BuildBIP44WalletAllocations(networkID uint32, numKeys int, amountPerKey uin _ = networkID // see comment above mnemonic := getMnemonicEnv() if mnemonic == "" { - return nil, fmt.Errorf("wallet allocations require MNEMONIC env var") + return nil, fmt.Errorf("wallet allocations require %s env var", MnemonicEnvVar) } if !bip39.IsMnemonicValid(mnemonic) { return nil, fmt.Errorf("invalid mnemonic for wallet key derivation") @@ -933,13 +908,13 @@ func BuildBIP44WalletAllocations(networkID uint32, numKeys int, amountPerKey uin return allocations, nil } -// BuildConfigFromEnv builds genesis config from environment variables -// Checks in order: KEYS_DIR, mnemonic (MNEMONIC/LIGHT_MNEMONIC), PRIVATE_KEY +// BuildConfigFromEnv builds genesis config from environment variables. +// Checks in order: KEYS_DIR, LUX_MNEMONIC, PRIVATE_KEY. // // Architecture: -// - X-Chain: 100 accounts × 500M LUX each, FREE -// - P-Chain: 100 accounts × 500M LUX each, vesting 1%/year from 2020-01-01 -// - C-Chain: treasury 0x9011...4714 gets 2T LUX +// - X-Chain: DefaultNumAccounts × allocationPerKey, immediate spend +// - P-Chain: DefaultNumAccounts × allocationPerKey, immediate spend +// - C-Chain: treasury 0x9011...4714 gets 2T LUX (no per-account alloc) func BuildConfigFromEnv(networkID uint32, numValidators int, allocationPerKey uint64) (*Config, error) { var err error @@ -960,7 +935,7 @@ func BuildConfigFromEnv(networkID uint32, numValidators int, allocationPerKey ui // m/44'/9000'/0'/0/i (the same path the genesis CLI's -bip44-wallet-keys // flag uses, and the same path the derive100 / luxfi wallet UIs use). // This is what every user-facing tool that scans the chain for funded - // addresses will expect — derive100 against $MNEMONIC and the + // addresses will expect — derive100 against $LUX_MNEMONIC and the // addresses here must match byte-for-byte. // // NOTE: this is intentionally a SEPARATE concern from diff --git a/pkg/genesis/light_mnemonic_guard_test.go b/pkg/genesis/light_mnemonic_guard_test.go index 052444e..d24dc6b 100644 --- a/pkg/genesis/light_mnemonic_guard_test.go +++ b/pkg/genesis/light_mnemonic_guard_test.go @@ -53,16 +53,16 @@ func TestIsProductionNetwork(t *testing.T) { } // TestIsKnownPublicMnemonic — F31 expansion. The guard now refuses every -// known-public mnemonic, not just LIGHT_MNEMONIC. The BIP-39 abandon +// known-public mnemonic, not just LightMnemonic. The BIP-39 abandon // vector, Hardhat default, and Trezor demo previously passed the -// LIGHT_MNEMONIC-only check. +// LightMnemonic-only check. func TestIsKnownPublicMnemonic(t *testing.T) { cases := []struct { name string mnemonic string want bool }{ - {"LIGHT_MNEMONIC", LightMnemonic, true}, + {"LightMnemonic", LightMnemonic, true}, {"BIP-39 abandon vector #1", "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", true}, @@ -93,48 +93,46 @@ func TestIsKnownPublicMnemonic(t *testing.T) { // TestRefuseLightMnemonicOnProduction is the headline F12 fix + // F31 expansion: derivation MUST refuse on production for any -// publicly-known mnemonic, not just the literal LIGHT_MNEMONIC. +// publicly-known mnemonic, not just the literal LightMnemonic. func TestRefuseLightMnemonicOnProduction(t *testing.T) { // 1. No mnemonic set: not our problem at this layer. - t.Setenv("MNEMONIC", "") - t.Setenv("LIGHT_MNEMONIC", "") + t.Setenv("LUX_MNEMONIC", "") if err := RefuseLightMnemonicOnProduction(constants.MainnetID); err != nil { t.Fatalf("no-mnemonic case: unexpected error: %v", err) } - // 2. LIGHT_MNEMONIC on dev mesh: allowed. - t.Setenv("LIGHT_MNEMONIC", LightMnemonic) + // 2. LightMnemonic on dev mesh: allowed. + t.Setenv("LUX_MNEMONIC", LightMnemonic) if err := RefuseLightMnemonicOnProduction(constants.LocalID); err != nil { - t.Fatalf("LIGHT_MNEMONIC on LocalID(1337): unexpected error: %v", err) + t.Fatalf("LightMnemonic on LocalID(1337): unexpected error: %v", err) } - // 3. LIGHT_MNEMONIC on mainnet: REFUSED. + // 3. LightMnemonic on mainnet: REFUSED. if err := RefuseLightMnemonicOnProduction(constants.MainnetID); err == nil { - t.Fatalf("LIGHT_MNEMONIC on MainnetID: expected refusal, got nil") + t.Fatalf("LightMnemonic on MainnetID: expected refusal, got nil") } - // 4. LIGHT_MNEMONIC on testnet: REFUSED. + // 4. LightMnemonic on testnet: REFUSED. if err := RefuseLightMnemonicOnProduction(constants.TestnetID); err == nil { - t.Fatalf("LIGHT_MNEMONIC on TestnetID: expected refusal, got nil") + t.Fatalf("LightMnemonic on TestnetID: expected refusal, got nil") } - // 5. F31: BIP-39 abandon vector via MNEMONIC on mainnet → REFUSED + // 5. F31: BIP-39 abandon vector via LUX_MNEMONIC on mainnet → REFUSED // (was previously ALLOWED — see prior red-review F31). - t.Setenv("LIGHT_MNEMONIC", "") - t.Setenv("MNEMONIC", "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about") + t.Setenv("LUX_MNEMONIC", "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about") if err := RefuseLightMnemonicOnProduction(constants.MainnetID); err == nil { t.Fatalf("BIP-39 abandon vector on mainnet: expected refusal, got nil") } // 6. F31: Hardhat default on mainnet → REFUSED. - t.Setenv("MNEMONIC", "test test test test test test test test test test test junk") + t.Setenv("LUX_MNEMONIC", "test test test test test test test test test test test junk") if err := RefuseLightMnemonicOnProduction(constants.MainnetID); err == nil { t.Fatalf("Hardhat default on mainnet: expected refusal, got nil") } // 7. Real (non-public) mnemonic on mainnet: allowed. realMnemonic := "venue armor mouse cheese fork stem siren acquire rocket cabbage sentence vibrant" - t.Setenv("MNEMONIC", realMnemonic) + t.Setenv("LUX_MNEMONIC", realMnemonic) if err := RefuseLightMnemonicOnProduction(constants.MainnetID); err != nil { t.Fatalf("real mnemonic on mainnet: unexpected error: %v", err) } @@ -147,19 +145,18 @@ func TestRefuseLightMnemonicOnProduction(t *testing.T) { // trusted callers), this one fails-closed on a production network with // any public mnemonic. func TestLoadKeysFromMnemonicEnvForNetwork(t *testing.T) { - // Setup: LIGHT_MNEMONIC on mainnet should refuse derivation. - t.Setenv("MNEMONIC", "") - t.Setenv("LIGHT_MNEMONIC", LightMnemonic) + // Setup: LightMnemonic on mainnet should refuse derivation. + t.Setenv("LUX_MNEMONIC", LightMnemonic) _, err := LoadKeysFromMnemonicEnvForNetwork(constants.MainnetID, 1) if err == nil { - t.Fatalf("LoadKeysFromMnemonicEnvForNetwork: LIGHT_MNEMONIC on mainnet must refuse, got nil") + t.Fatalf("LoadKeysFromMnemonicEnvForNetwork: LightMnemonic on mainnet must refuse, got nil") } // Same env on a dev mesh should succeed (the guard does not refuse; // derivation proceeds via the underlying LoadKeysFromMnemonicEnv). keys, err := LoadKeysFromMnemonicEnvForNetwork(constants.LocalID, 1) if err != nil { - t.Fatalf("LoadKeysFromMnemonicEnvForNetwork: LIGHT_MNEMONIC on dev (LocalID=1337): unexpected error: %v", err) + t.Fatalf("LoadKeysFromMnemonicEnvForNetwork: LightMnemonic on dev (LocalID=1337): unexpected error: %v", err) } if len(keys) != 1 { t.Fatalf("expected 1 key, got %d", len(keys)) diff --git a/pkg/genesis/networks.go b/pkg/genesis/networks.go new file mode 100644 index 0000000..8f4d1ab --- /dev/null +++ b/pkg/genesis/networks.go @@ -0,0 +1,45 @@ +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package genesis + +import "github.com/luxfi/constants" + +// Canonical network ID map — the only legal values for the primary +// network ID. Anything else is "custom" and a bug. +// +// primary network ID EVM (C-Chain) chain ID +// ────────────────── ────────────────────── +// constants.MainnetID (1) constants.MainnetChainID (96369) +// constants.TestnetID (2) constants.TestnetChainID (96368) +// constants.DevnetID (3) constants.DevnetChainID (96370) +// constants.LocalID (1337) constants.LocalChainID (31337) +// +// The primary network ID is SHARED across all Lux-derived L1s (Lux, +// Hanzo, Zoo, Pars, …). EVM chain IDs are per-subnet, per-env. +// +// Mnemonic source (LUX_MNEMONIC): +// - mainnet/testnet/devnet (1/2/3): private hardware-RNG mnemonic from +// KMS; RefuseLightMnemonicOnProduction enforces this. +// - local (1337): LightMnemonic is the well-known dev seed value. +// +// All four envs read the mnemonic from the same env var (LUX_MNEMONIC); +// per-env isolation comes from a different mnemonic per env, not from +// the env-var name. +var CanonicalPrimaryNetworkIDs = []uint32{ + constants.MainnetID, // 1 + constants.TestnetID, // 2 + constants.DevnetID, // 3 + constants.LocalID, // 1337 +} + +// IsCanonicalPrimaryNetwork reports whether networkID is one of the four +// canonical primary network IDs. Custom IDs (e.g. UnitTestID) are not +// canonical and callers should treat them explicitly. +func IsCanonicalPrimaryNetwork(networkID uint32) bool { + switch networkID { + case constants.MainnetID, constants.TestnetID, constants.DevnetID, constants.LocalID: + return true + } + return false +} diff --git a/pkg/genesis/validator_keys.go b/pkg/genesis/validator_keys.go index 2955a9a..9b9ee84 100644 --- a/pkg/genesis/validator_keys.go +++ b/pkg/genesis/validator_keys.go @@ -27,13 +27,8 @@ const ( // DefaultValidatorAllocation is 1B LUX per validator DefaultValidatorAllocation uint64 = OneBillionLUX - - // VestingPeriods is 100 years (1% unlocks per year) - VestingPeriods = 100 ) -// StakingStartTime and UnlockInterval are defined in keys.go - // ValidatorKeyInfo contains computed addresses from a validator's private key type ValidatorKeyInfo struct { PrivKeyHex string // Hex-encoded private key @@ -167,42 +162,6 @@ func GeneratePChainAllocations(keys []ValidatorKeyInfo, hrp string, amountPerKey return allocations, nil } -// GeneratePChainAllocationsWithVesting creates P-chain allocations with a vesting schedule. -// Each key gets the specified amount vested over the given number of periods. -func GeneratePChainAllocationsWithVesting(keys []ValidatorKeyInfo, hrp string, amountPerKey uint64, startTime uint64, interval uint64, periods int) ([]AllocationJSON, error) { - if amountPerKey == 0 { - amountPerKey = OneBillionLUX - } - if startTime == 0 { - startTime = StakingStartTime - } - if interval == 0 { - interval = UnlockInterval - } - if periods == 0 { - periods = 100 // 100 years default - } - - allocations := make([]AllocationJSON, len(keys)) - for i, key := range keys { - utxoAddr, err := FormatChainAddress("P", hrp, key.ShortID) - if err != nil { - return nil, fmt.Errorf("failed to format address for key %d: %w", i, err) - } - - // Build vesting schedule - unlockSchedule := buildUnlockSchedule(amountPerKey, startTime, interval, periods) - - allocations[i] = AllocationJSON{ - EVMAddr: key.EVMAddr, - UTXOAddr: utxoAddr, - InitialAmount: amountPerKey, // X-chain initial amount - UnlockSchedule: unlockSchedule, - } - } - return allocations, nil -} - // GenerateCChainAlloc creates C-chain genesis allocations for validator keys. func GenerateCChainAlloc(keys []ValidatorKeyInfo, amount uint64) map[string]Balance { if amount == 0 {