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 ## 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 ### Breaking — HIP-0077 §"Identity" HD path alignment
`pkg/genesis/keys.go` now derives keys on the per-network hardened `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. re-derived after upgrading.
- If you need to keep the old addresses spendable, export their private - If you need to keep the old addresses spendable, export their private
keys (`BuildWalletKeyHex` on the old code) and load them via 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`: Per CLAUDE.md `no backwards compatibility, only forwards perfection`:
this is the correct break; clients deriving from the HIP-0077 spec 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) | | `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 | | `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 | | `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` | | `2b16f45` | v1.9.5 | Operator-overridable C-Chain genesis via `LUX_CCHAIN_GENESIS_FILE` |
@@ -350,7 +350,7 @@ Generate genesis configurations:
```bash ```bash
# Generate from mnemonic # 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 # From existing keys
./genesis -network mainnet -keys-dir ~/.lux/keys -output genesis.json ./genesis -network mainnet -keys-dir ~/.lux/keys -output genesis.json
+2 -8
View File
@@ -8,15 +8,9 @@ import (
) )
func main() { func main() {
mnemonic := "" mnemonic := os.Getenv(genesis.MnemonicEnvVar)
for _, env := range []string{"MNEMONIC", "LIGHT_MNEMONIC"} {
if v := os.Getenv(env); v != "" {
mnemonic = v
break
}
}
if mnemonic == "" { 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) 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, "usage: derive100 <hrp> <rpc> <mnemonic-env>")
fmt.Fprintln(os.Stderr, " hrp: test|lux|dev|local") fmt.Fprintln(os.Stderr, " hrp: test|lux|dev|local")
fmt.Fprintln(os.Stderr, " rpc: http://24.199.71.151:9650") 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) os.Exit(1)
} }
hrp := os.Args[1] hrp := os.Args[1]
+6 -6
View File
@@ -42,7 +42,7 @@ func main() {
output = flag.String("output", "", "Output file path (default: stdout)") output = flag.String("output", "", "Output file path (default: stdout)")
allocation = flag.Uint64("allocation", genesis.DefaultAllocationPerValidator, "Allocation per validator in nLUX") allocation = flag.Uint64("allocation", genesis.DefaultAllocationPerValidator, "Allocation per validator in nLUX")
validators = flag.Int("validators", 3, "Number of validators for mnemonic-based genesis") 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)") 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)") 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)") cchainPath = flag.String("cchain", "", "Path to existing C-Chain genesis (preserves original)")
@@ -115,10 +115,10 @@ Flags:
Environment Variables: Environment Variables:
KEYS_DIR Directory containing node keys 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) PRIVATE_KEY Single private key (hex)
GENESIS_DIR Directory containing genesis component files GENESIS_DIR Directory containing genesis component files
NETWORK_ID Network ID NETWORK_ID Network ID
Examples: Examples:
# Generate local genesis from keys in ~/.lux/keys # Generate local genesis from keys in ~/.lux/keys
@@ -128,7 +128,7 @@ Examples:
genesis -network mainnet -cchain mainnet/cchain.json -output primary.json genesis -network mainnet -cchain mainnet/cchain.json -output primary.json
# Generate from mnemonic with 3 validators # 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 # Load from existing genesis directory
genesis -genesis-dir ./mainnet -output primary.json`) 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) config, err = genesis.BuildConfigFromKeys(networkID, keysDir, allocation)
if err != nil { 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) 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 // 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 // of the same secp256k1 spending key, computed from the BIP44 child
// at index 0 (not the Lux-internal hardened path). // at index 0 (not the Lux-internal hardened path).
first := allocs[0].(map[string]interface{}) first := allocs[0].(map[string]interface{})
+18 -76
View File
@@ -9,36 +9,19 @@ import (
) )
// ChainAllocations holds genesis allocations for all chains. // 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 { type ChainAllocations struct {
keys []ValidatorKeyInfo keys []ValidatorKeyInfo
hrp string hrp string
amount uint64 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. // 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 { func NewAllocations(keys []ValidatorKeyInfo, hrp string) *ChainAllocations {
return &ChainAllocations{ return &ChainAllocations{
keys: keys, keys: keys,
@@ -53,14 +36,8 @@ func (a *ChainAllocations) WithAmount(amount uint64) *ChainAllocations {
return a 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. // PChain returns P-chain allocations in the standard AllocationJSON format.
// All entries are immediately spendable (locktime=0).
func (a *ChainAllocations) PChain() ([]AllocationJSON, error) { func (a *ChainAllocations) PChain() ([]AllocationJSON, error) {
allocations := make([]AllocationJSON, len(a.keys)) 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) 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{ allocations[i] = AllocationJSON{
EVMAddr: key.EVMAddr, EVMAddr: key.EVMAddr,
UTXOAddr: utxoAddr, UTXOAddr: utxoAddr,
InitialAmount: initialAmount, InitialAmount: a.amount,
UnlockSchedule: unlockSchedule, UnlockSchedule: []LockedAmount{{Amount: a.amount, Locktime: 0}},
} }
} }
return allocations, nil 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). // 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) { func (a *ChainAllocations) PChainMap() ([]map[string]interface{}, error) {
allocations := make([]map[string]interface{}, len(a.keys)) 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) 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{}{ allocations[i] = map[string]interface{}{
"evmAddr": key.EVMAddr, "evmAddr": key.EVMAddr,
"utxoAddr": utxoAddr, "utxoAddr": utxoAddr,
"initialAmount": initialAmount, "initialAmount": a.amount,
"unlockSchedule": unlockSchedule, "unlockSchedule": []map[string]interface{}{
{"amount": a.amount, "locktime": uint64(0)},
},
} }
} }
return allocations, nil return allocations, nil
@@ -206,15 +149,14 @@ func QuickAllocations(keys []ValidatorKeyInfo, hrp string, amount uint64) (*AllC
return NewAllocations(keys, hrp).WithAmount(amount).All() 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) { func MainnetAllocations(keys []ValidatorKeyInfo, hrp string) (*AllChainAllocations, error) {
return NewAllocations(keys, hrp). return NewAllocations(keys, hrp).
WithAmount(OneBillionLUX). WithAmount(OneBillionLUX).
WithVesting(DefaultVesting()).
All() 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) { func TestnetAllocations(keys []ValidatorKeyInfo, hrp string) (*AllChainAllocations, error) {
return NewAllocations(keys, hrp). return NewAllocations(keys, hrp).
WithAmount(OneHundredMillionLUX). WithAmount(OneHundredMillionLUX).
+67 -92
View File
@@ -34,53 +34,46 @@ import (
) )
const ( const (
// DefaultAllocationPerAccount is 50M LUX per account per chain (P and X). // DefaultAllocationPerAccount is the canonical fresh-genesis amount:
// 1000 accounts × 50M × 2 chains = 100B LUX of UTXOs in genesis (well // 50M LUX per derived account, spendable immediately on both X-Chain
// under the 2T LUX SupplyCap on every network). // and P-Chain (same address — bech32(ripemd160(sha256(secp256k1_pubkey)))).
// X-Chain: 50M free (immediately spendable from genesis). //
// P-Chain: 50M free at genesis (no vesting); the long-tail unlock // 1000 accounts × 50M × 2 chains = 100B LUX of UTXOs in genesis.
// schedule below adds a separate 50M per account that vests 1%/year // Per-chain SupplyCap is 2T LUX, so each chain individually clears.
// over 100 years from Jan 1 2020 — i.e. each address sees 50M
// spendable on X, 50M spendable on P + 50M vesting on P.
// //
// Address scheme — Bitcoin-UTXO-style (more quantum-resistant): // Address scheme — Bitcoin-UTXO-style (more quantum-resistant):
// P-Chain / X-Chain addresses are bech32(ripemd160(sha256(pubkey))). // P-Chain / X-Chain addresses are bech32(ripemd160(sha256(pubkey))).
// The public key is *hidden behind two hash layers* until the // The public key is hidden behind two hash layers until first spend.
// address is first spent. Until first spend, even a future // A future quantum adversary cannot recover the private key from the
// quantum adversary cannot recover the private key — Shor's // address alone — Shor's works on the pubkey, not on sha256+ripemd160.
// algorithm works against the secp256k1 pubkey, but not against // C-Chain (Ethereum) addresses expose the pubkey via ECDSA recovery
// sha256+ripemd160. Contrast with C-Chain (Ethereum) addresses, // on every signed tx; long-term holds belong on P/X, not C.
// 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.
DefaultAllocationPerAccount = 50_000_000 * Lux DefaultAllocationPerAccount = 50_000_000 * Lux
// DefaultAllocationPerValidator is kept for backward compatibility // DefaultAllocationPerValidator is kept for backward compatibility
DefaultAllocationPerValidator = DefaultAllocationPerAccount DefaultAllocationPerValidator = DefaultAllocationPerAccount
// DefaultNumAccounts is the default number of mnemonic-derived accounts // DefaultNumAccounts is the default number of mnemonic-derived accounts.
// Funds 1000 wallet keys at canonical BIP44 m/44'/9000'/0'/0/i so that // Funds 1000 BIP44 wallet keys at m/44'/9000'/0'/0/i so that any
// any wallet that derives at this path against the SAME mnemonic sees // canonical-BIP44 wallet (Core, MetaMask, AvalancheJS, `lux key derive`)
// a fundable address on both P and X. // 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 DefaultNumAccounts = 1000
// StakingStartTime is Jan 1, 2020 00:00:00 UTC // TreasuryAddress is the C-Chain treasury (no per-account C-Chain alloc).
StakingStartTime = 1577836800
// UnlockInterval is 1 year in seconds
UnlockInterval = 365 * 24 * 60 * 60
// TreasuryAddress is the C-Chain treasury with 2T LUX
TreasuryAddress = "0x9011E888251AB053B7bD1cdB598Db4f9DEd94714" 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 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 PChainFeeReserve = 10_000 * Lux
// LightMnemonic is the well-known dev mnemonic for local networks (network-id >= 1337). // LightMnemonic is the well-known public dev seed. Pass this as the
// NEVER use on public networks (mainnet, testnet, devnet). // 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" 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 }, 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. // buildCChainGenesisTreasury creates C-chain genesis JSON with only the treasury allocation.
// Treasury: 0x9011E888251AB053B7bD1cdB598Db4f9DEd94714 gets 2T LUX. // Treasury: 0x9011E888251AB053B7bD1cdB598Db4f9DEd94714 gets 2T LUX.
// No mnemonic-derived account allocations on C-Chain. // No mnemonic-derived account allocations on C-Chain.
@@ -539,14 +517,14 @@ func mldsaKeygenFromChildSeed(childSeed []byte) ([]byte, error) {
return pk.Bytes(), nil 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 // Callers that already know the network id MUST use
// LoadKeysFromMnemonicEnvForNetwork instead — it adds the production-safe // LoadKeysFromMnemonicEnvForNetwork instead — it adds the production-safe
// public-mnemonic guard around this entry point. // public-mnemonic guard around this entry point.
func LoadKeysFromMnemonicEnv(numAccounts int) ([]KeyInfo, error) { func LoadKeysFromMnemonicEnv(numAccounts int) ([]KeyInfo, error) {
mnemonic := getMnemonicEnv() mnemonic := getMnemonicEnv()
if mnemonic == "" { 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) return LoadKeysFromMnemonic(mnemonic, numAccounts)
} }
@@ -630,28 +608,24 @@ func genesisMessage(networkID uint32) string {
} }
} }
// getMnemonicEnv returns the mnemonic from environment variables. // MnemonicEnvVar is the single canonical env var name for the genesis
// Priority: MNEMONIC > LIGHT_MNEMONIC // mnemonic. One env, one way:
// // - mainnet/testnet/devnet (IDs 1/2/3): a private hardware-RNG mnemonic
// LIGHT_MNEMONIC is the publicly-known dev seed. Anyone in the world can // loaded from KMS; RefuseLightMnemonicOnProduction rejects all known
// derive its first 200 child keys; the auto-fund pre-allocation in // public mnemonics so the production guard is enforced at derivation.
// HIP-0077 §"Auto-funding the first 200 devices" is *only* meant for // - local (ID 1337): typically set to LightMnemonic for fast dev boot;
// network IDs >= 1337 (dev / primary local mesh). Production networks // the same public-mnemonic blacklist still applies, but local IDs
// MUST set MNEMONIC. // are exempt by IsProductionNetwork.
// const MnemonicEnvVar = "LUX_MNEMONIC"
// Use IsLightMnemonic and RefuseLightMnemonicOnProduction to enforce that
// rule wherever a key is loaded for a known network ID. // getMnemonicEnv returns the mnemonic from the canonical LUX_MNEMONIC env
// var. No fallback chain — one and only one env name.
func getMnemonicEnv() string { func getMnemonicEnv() string {
for _, env := range []string{"MNEMONIC", "LIGHT_MNEMONIC"} { return os.Getenv(MnemonicEnvVar)
if v := os.Getenv(env); v != "" {
return v
}
}
return ""
} }
// IsLightMnemonic reports whether the given mnemonic is exactly the // 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. // timing attacker can't probe the running config from outside.
func IsLightMnemonic(mnemonic string) bool { func IsLightMnemonic(mnemonic string) bool {
return subtleConstantTimeEqual([]byte(mnemonic), []byte(LightMnemonic)) 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 // knownPublicMnemonics is the curated set of seeds that anyone in the
// world can derive from. Production deployments MUST refuse all of them. // 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 // mnemonics from BIP-39 test vectors, common dev tooling defaults, and
// hardware-wallet demo seeds. Any of these on a production network → // hardware-wallet demo seeds. Any of these on a production network →
// every derived child key is publicly enumerable. // every derived child key is publicly enumerable.
@@ -687,7 +661,7 @@ var knownPublicMnemonics = []string{
} }
// IsKnownPublicMnemonic reports whether the given mnemonic appears in any // 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 // default, hardware-wallet demos, etc.). Compared in constant time per
// entry so a timing attacker can't probe which entry matched. // 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 // IsProductionNetwork reports whether the given numeric network ID is on
// the list of *production* Lux networks. Local / primary-local meshes // the list of *production* Lux networks. Local / primary-local meshes
// (network IDs >= 1337, including constants.LocalID = 1337) deliberately // (network IDs >= 1337, including constants.LocalID = 1337) deliberately
// allow LIGHT_MNEMONIC; mainnet, testnet and any other reserved low-ID // allow public mnemonics like LightMnemonic; mainnet, testnet and any
// network refuse it. // other reserved low-ID network refuse them.
// //
// Network ID convention (mirrors lux/constants): // Canonical network ID map (mirrors lux/constants):
// - 1 mainnet (production) // - 1 mainnet (production — refuses public mnemonics)
// - 2 testnet (production-grade staging — refuses LIGHT_MNEMONIC) // - 2 testnet (production-grade staging — refuses public mnemonics)
// - 1337 LocalID (free-form local dev, allows LIGHT_MNEMONIC) // - 3 devnet (production-grade dev mesh — refuses public mnemonics)
// - >= 1337 any tenant local / dev mesh (allows LIGHT_MNEMONIC) // - 1337 LocalID (free-form local dev allows LightMnemonic)
// - 3..1336 reserved; treated as production by default // - >= 1337 any tenant local / dev mesh (allows LightMnemonic)
// - 4..1336 reserved; treated as production by default
func IsProductionNetwork(networkID uint32) bool { func IsProductionNetwork(networkID uint32) bool {
switch networkID { switch networkID {
case constants.MainnetID, constants.TestnetID: case constants.MainnetID, constants.TestnetID:
@@ -730,7 +705,7 @@ func IsProductionNetwork(networkID uint32) bool {
} }
// RefuseLightMnemonicOnProduction returns a non-nil error iff the running // 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 // BIP-39 test vectors, Hardhat / Trezor demos, …) AND the supplied
// networkID is a production network. Runtime guard required by HIP-0077 // networkID is a production network. Runtime guard required by HIP-0077
// §"Mnemonic exposure" / "Auto-funded blast radius". // §"Mnemonic exposure" / "Auto-funded blast radius".
@@ -743,9 +718,9 @@ func IsProductionNetwork(networkID uint32) bool {
// derivation point, never silently produces public-mnemonic-derived // derivation point, never silently produces public-mnemonic-derived
// signing keys. // signing keys.
// //
// The guard widens beyond LIGHT_MNEMONIC to cover the broader // The guard covers the full public-mnemonic blacklist (HIP-0077 red-review
// public-mnemonic blacklist (HIP-0077 red-review F31): BIP-39 abandon // F31): LightMnemonic, BIP-39 abandon vector, Hardhat default, Trezor
// vector, Hardhat default, Trezor demo, etc. See knownPublicMnemonics. // demo, etc. See knownPublicMnemonics.
func RefuseLightMnemonicOnProduction(networkID uint32) error { func RefuseLightMnemonicOnProduction(networkID uint32) error {
mnemonic := getMnemonicEnv() mnemonic := getMnemonicEnv()
if mnemonic == "" { if mnemonic == "" {
@@ -761,11 +736,11 @@ func RefuseLightMnemonicOnProduction(networkID uint32) error {
} }
return fmt.Errorf( return fmt.Errorf(
"refusing to derive keys: a publicly-known mnemonic is set on production "+ "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 "+ "BIP-39 test vectors, Hardhat/Trezor demos) are deterministic — anyone "+
"can derive every child key. Set MNEMONIC env var with "+ "can derive every child key. Set %s with a private hardware-RNG "+
"a private hardware-RNG mnemonic loaded from KMS, or run on a dev "+ "mnemonic loaded from KMS, or run on a dev network ID (>= 1337)",
"network ID (>= 1337)", networkID, networkID, MnemonicEnvVar,
) )
} }
@@ -776,7 +751,7 @@ func RefuseLightMnemonicOnProduction(networkID uint32) error {
// //
// Closes HIP-0077 red-review F30 (the prior LoadKeysFromMnemonicEnv was // Closes HIP-0077 red-review F30 (the prior LoadKeysFromMnemonicEnv was
// guard-free and operators could silently derive on production from // guard-free and operators could silently derive on production from
// LIGHT_MNEMONIC). // the public LightMnemonic).
func LoadKeysFromMnemonicEnvForNetwork(networkID uint32, numAccounts int) ([]KeyInfo, error) { func LoadKeysFromMnemonicEnvForNetwork(networkID uint32, numAccounts int) ([]KeyInfo, error) {
if err := RefuseLightMnemonicOnProduction(networkID); err != nil { if err := RefuseLightMnemonicOnProduction(networkID); err != nil {
return nil, err return nil, err
@@ -868,7 +843,7 @@ func BuildBIP44WalletAllocations(networkID uint32, numKeys int, amountPerKey uin
_ = networkID // see comment above _ = networkID // see comment above
mnemonic := getMnemonicEnv() mnemonic := getMnemonicEnv()
if mnemonic == "" { 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) { if !bip39.IsMnemonicValid(mnemonic) {
return nil, fmt.Errorf("invalid mnemonic for wallet key derivation") 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 return allocations, nil
} }
// BuildConfigFromEnv builds genesis config from environment variables // BuildConfigFromEnv builds genesis config from environment variables.
// Checks in order: KEYS_DIR, mnemonic (MNEMONIC/LIGHT_MNEMONIC), PRIVATE_KEY // Checks in order: KEYS_DIR, LUX_MNEMONIC, PRIVATE_KEY.
// //
// Architecture: // Architecture:
// - X-Chain: 100 accounts × 500M LUX each, FREE // - X-Chain: DefaultNumAccounts × allocationPerKey, immediate spend
// - P-Chain: 100 accounts × 500M LUX each, vesting 1%/year from 2020-01-01 // - P-Chain: DefaultNumAccounts × allocationPerKey, immediate spend
// - C-Chain: treasury 0x9011...4714 gets 2T LUX // - C-Chain: treasury 0x9011...4714 gets 2T LUX (no per-account alloc)
func BuildConfigFromEnv(networkID uint32, numValidators int, allocationPerKey uint64) (*Config, error) { func BuildConfigFromEnv(networkID uint32, numValidators int, allocationPerKey uint64) (*Config, error) {
var err 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 // 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). // 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 // 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. // addresses here must match byte-for-byte.
// //
// NOTE: this is intentionally a SEPARATE concern from // 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 // 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 // vector, Hardhat default, and Trezor demo previously passed the
// LIGHT_MNEMONIC-only check. // LightMnemonic-only check.
func TestIsKnownPublicMnemonic(t *testing.T) { func TestIsKnownPublicMnemonic(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
mnemonic string mnemonic string
want bool want bool
}{ }{
{"LIGHT_MNEMONIC", LightMnemonic, true}, {"LightMnemonic", LightMnemonic, true},
{"BIP-39 abandon vector #1", {"BIP-39 abandon vector #1",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
true}, true},
@@ -93,48 +93,46 @@ func TestIsKnownPublicMnemonic(t *testing.T) {
// TestRefuseLightMnemonicOnProduction is the headline F12 fix + // TestRefuseLightMnemonicOnProduction is the headline F12 fix +
// F31 expansion: derivation MUST refuse on production for any // 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) { func TestRefuseLightMnemonicOnProduction(t *testing.T) {
// 1. No mnemonic set: not our problem at this layer. // 1. No mnemonic set: not our problem at this layer.
t.Setenv("MNEMONIC", "") t.Setenv("LUX_MNEMONIC", "")
t.Setenv("LIGHT_MNEMONIC", "")
if err := RefuseLightMnemonicOnProduction(constants.MainnetID); err != nil { if err := RefuseLightMnemonicOnProduction(constants.MainnetID); err != nil {
t.Fatalf("no-mnemonic case: unexpected error: %v", err) t.Fatalf("no-mnemonic case: unexpected error: %v", err)
} }
// 2. LIGHT_MNEMONIC on dev mesh: allowed. // 2. LightMnemonic on dev mesh: allowed.
t.Setenv("LIGHT_MNEMONIC", LightMnemonic) t.Setenv("LUX_MNEMONIC", LightMnemonic)
if err := RefuseLightMnemonicOnProduction(constants.LocalID); err != nil { 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 { 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 { 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). // (was previously ALLOWED — see prior red-review F31).
t.Setenv("LIGHT_MNEMONIC", "") t.Setenv("LUX_MNEMONIC", "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
t.Setenv("MNEMONIC", "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
if err := RefuseLightMnemonicOnProduction(constants.MainnetID); err == nil { if err := RefuseLightMnemonicOnProduction(constants.MainnetID); err == nil {
t.Fatalf("BIP-39 abandon vector on mainnet: expected refusal, got nil") t.Fatalf("BIP-39 abandon vector on mainnet: expected refusal, got nil")
} }
// 6. F31: Hardhat default on mainnet → REFUSED. // 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 { if err := RefuseLightMnemonicOnProduction(constants.MainnetID); err == nil {
t.Fatalf("Hardhat default on mainnet: expected refusal, got nil") t.Fatalf("Hardhat default on mainnet: expected refusal, got nil")
} }
// 7. Real (non-public) mnemonic on mainnet: allowed. // 7. Real (non-public) mnemonic on mainnet: allowed.
realMnemonic := "venue armor mouse cheese fork stem siren acquire rocket cabbage sentence vibrant" 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 { if err := RefuseLightMnemonicOnProduction(constants.MainnetID); err != nil {
t.Fatalf("real mnemonic on mainnet: unexpected error: %v", err) 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 // trusted callers), this one fails-closed on a production network with
// any public mnemonic. // any public mnemonic.
func TestLoadKeysFromMnemonicEnvForNetwork(t *testing.T) { func TestLoadKeysFromMnemonicEnvForNetwork(t *testing.T) {
// Setup: LIGHT_MNEMONIC on mainnet should refuse derivation. // Setup: LightMnemonic on mainnet should refuse derivation.
t.Setenv("MNEMONIC", "") t.Setenv("LUX_MNEMONIC", LightMnemonic)
t.Setenv("LIGHT_MNEMONIC", LightMnemonic)
_, err := LoadKeysFromMnemonicEnvForNetwork(constants.MainnetID, 1) _, err := LoadKeysFromMnemonicEnvForNetwork(constants.MainnetID, 1)
if err == nil { 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; // Same env on a dev mesh should succeed (the guard does not refuse;
// derivation proceeds via the underlying LoadKeysFromMnemonicEnv). // derivation proceeds via the underlying LoadKeysFromMnemonicEnv).
keys, err := LoadKeysFromMnemonicEnvForNetwork(constants.LocalID, 1) keys, err := LoadKeysFromMnemonicEnvForNetwork(constants.LocalID, 1)
if err != nil { 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 { if len(keys) != 1 {
t.Fatalf("expected 1 key, got %d", len(keys)) 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 is 1B LUX per validator
DefaultValidatorAllocation uint64 = OneBillionLUX 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 // ValidatorKeyInfo contains computed addresses from a validator's private key
type ValidatorKeyInfo struct { type ValidatorKeyInfo struct {
PrivKeyHex string // Hex-encoded private key PrivKeyHex string // Hex-encoded private key
@@ -167,42 +162,6 @@ func GeneratePChainAllocations(keys []ValidatorKeyInfo, hrp string, amountPerKey
return allocations, nil 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. // GenerateCChainAlloc creates C-chain genesis allocations for validator keys.
func GenerateCChainAlloc(keys []ValidatorKeyInfo, amount uint64) map[string]Balance { func GenerateCChainAlloc(keys []ValidatorKeyInfo, amount uint64) map[string]Balance {
if amount == 0 { if amount == 0 {