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.
This commit is contained in:
Hanzo AI
2026-06-02 22:21:27 -07:00
parent 210eb824e7
commit 4383ad4583
11 changed files with 201 additions and 251 deletions
+39 -1
View File
@@ -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
+2 -2
View File
@@ -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
+2 -8
View File
@@ -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)
}
+1 -1
View File
@@ -129,7 +129,7 @@ func main() {
fmt.Fprintln(os.Stderr, "usage: derive100 <hrp> <rpc> <mnemonic-env>")
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]
+4 -4
View File
@@ -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,7 +115,7 @@ 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
@@ -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)
+1 -1
View File
@@ -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{})
+16 -74
View File
@@ -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,
"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).
+67 -92
View File
@@ -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
+20 -23
View File
@@ -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))
+45
View File
@@ -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
}
-41
View File
@@ -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 {