fix(mainnet): repair P/X genesis supply overshoot (50T -> 50B, 2T cap)

Mainnet primary-network genesis funded 100 P-Chain allocations of 5e17
base units each. Those are stale 9-decimal magnitudes read under the
current 6-decimal LUX unit (units.Lux = 1e6 microLUX), so each was
mis-read as 500B LUX -> P-total 50T = 25x the 2T supply cap, mirrored
on X-Chain. The sum (5e19 base units) also overflows uint64 and wraps,
which is how live mainnet showed ~13.27T P-supply.

Replace the mainnet allocation set with the canonical uniform set that
testnet/devnet already use: 1000 BIP44 wallet keys (m/44'/9000'/0'/0/i,
coin_type 9000) x 50M LUX, re-encoded for mainnet's `lux` HRP via the
canonical address.Format path (NOT hand-rolled bech32). P-total now
50.0B LUX, 40x under the 2T cap.

Authoritative source is the split shard pchain.json (configs.GetGenesis
assembles from network.json + pchain.json + {x,c,...}chain.json shards;
the combined genesis.json is a derived human-facing snapshot). Both are
updated; the combined snapshot is regenerated from the corrected shards
so it faithfully matches what luxd builds (this also un-stales its
cChainGenesis blob, which lagged the cchain.json shard).

Correctness proof:
- generator reproduces devnet's 1000 funded allocations byte-identically
- mainnet regenerated evmAddrs == testnet's 1000 canonical funded
  evmAddrs (same mnemonic -> same keys; evmAddr is HRP-independent)
- utxoAddr differs from testnet only by HRP (lux1.. vs test1..);
  regenerated lux-HRP addresses validated against the live testnet file
- all 5 initialStakedFunds / initialStakers reward addresses (BIP44
  keys 0-4) remain funded -> luxd staked-funds invariant holds
- initialStakers (NodeIDs + BLS PoP) preserved verbatim -> live
  validator identity unchanged

Preserved byte-identical: cChainGenesis (2T treasury @
0x9011E888..4714, chainId 96369, immutable), all other chain shards,
networkID 1, startTime, initialStakers, initialStakedFunds.

X-Chain descriptor denomination 9 -> 6 (xchain.json + genesis.json).
denomination is the LUX asset decimal count and is serialized into the
asset definition (feeds AssetIDFromBytes) -> consensus-relevant. The
canonical unit is 6 decimals; the stale 9 is corrected for mainnet
ahead of its fresh re-genesis. testnet/devnet/localnet left unchanged.

Reconciliation: 2T C-Chain treasury + 100B (P+X user accounts) = 2.1T
at genesis, reduced to 2T max by the 2025-12-25 burn fork (not
implemented here; arithmetic is the intended model).

Tooling (reusable, used to produce this fix):
- cmd/genalloc: emit canonical BIP44 allocations for one network,
  scoped to the allocations array only (never touches cChainGenesis)
- cmd/assemblegenesis: regenerate the combined genesis.json snapshot
  from the embedded shards via the same GetGenesis path luxd uses

Staging only; mainnet re-genesis relaunch is gated separately by the
owner. testnet/devnet/localnet untouched.
This commit is contained in:
zeekay
2026-06-24 14:52:16 -07:00
parent 0534c85d2e
commit ad880ffb8d
6 changed files with 20817 additions and 80032 deletions
+63
View File
@@ -0,0 +1,63 @@
// Copyright (C) 2019-2025, Lux Partners Limited. All rights reserved.
// See the file LICENSE for licensing terms.
// Command assemblegenesis writes the canonical combined genesis.json for a
// network by assembling it from the embedded split shards (network.json +
// pchain.json + {x,c,d,q,a,b,t,z,g,k}chain.json) via the exact same
// configs.GetGenesis path luxd uses at boot. The combined genesis.json is a
// human-facing snapshot; the shards are the source of truth. Run this after
// editing any shard so the snapshot stays faithful to what the node builds.
//
// Usage:
//
// assemblegenesis -network-id 1 -output configs/mainnet/genesis.json
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"os"
"github.com/luxfi/genesis/configs"
)
func main() {
networkID := flag.Uint("network-id", 0, "Network ID (1=mainnet, 2=testnet, 3=devnet, 1337=local)")
output := flag.String("output", "", "Output file path (default: stdout)")
flag.Parse()
if *networkID == 0 {
fmt.Fprintln(os.Stderr, "Error: -network-id is required")
os.Exit(2)
}
// GetGenesis assembles from the embedded shards exactly as luxd does.
data, err := configs.GetGenesis(uint32(*networkID))
if err != nil {
fmt.Fprintf(os.Stderr, "Error assembling genesis: %v\n", err)
os.Exit(1)
}
// Pretty-print for the human-facing snapshot (2-space indent, matching
// the existing committed shape). Parsing is format-agnostic, so indent
// choice is cosmetic — but consistency keeps diffs readable.
var pretty bytes.Buffer
if err := json.Indent(&pretty, data, "", " "); err != nil {
fmt.Fprintf(os.Stderr, "Error pretty-printing genesis: %v\n", err)
os.Exit(1)
}
pretty.WriteByte('\n')
if *output == "" {
os.Stdout.Write(pretty.Bytes())
return
}
if err := os.WriteFile(*output, pretty.Bytes(), 0o644); err != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", *output, err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Wrote combined genesis for networkID %d to %s (%d bytes)\n",
*networkID, *output, pretty.Len())
}
+108
View File
@@ -0,0 +1,108 @@
// Copyright (C) 2019-2025, Lux Partners Limited. All rights reserved.
// See the file LICENSE for licensing terms.
// Command genalloc emits the canonical BIP44 wallet allocations array for a
// single network, in the exact AllocationJSON shape the primary-network
// genesis files use (initialAmount + a single locktime-0 unlockSchedule
// entry, utxoAddr in the network's bech32 HRP, evmAddr in 0x H160 form).
//
// It is deliberately scoped to ONLY the allocations array: it does not
// touch cChainGenesis or any other chain blob. Splice its output into an
// existing genesis.json to repair/replace the funded user-account set
// without disturbing the immutable C-Chain treasury or validator config.
//
// The derivation is the canonical BIP44 path m/44'/9000'/0'/0/i
// (LoadBIP44WalletKeysFromMnemonic) — the same keys every Lux web wallet,
// `lux key derive`, and the testnet/devnet genesis already fund. The
// network HRP comes from constants.GetHRP(networkID); the 20-byte UTXO and
// EVM payloads are HRP-independent, so the same mnemonic yields identical
// evmAddrs across all networks (only the utxoAddr HRP differs).
//
// Usage:
//
// LUX_MNEMONIC="$(cat /path/to/mnemonic)" \
// genalloc -network-id 1 -keys 1000 -amount 50000000 > alloc.json
//
// -network-id Network ID (1=mainnet/lux, 2=testnet/test, 3=devnet/dev, 1337=local)
// -keys Number of canonical BIP44 wallet keys to fund
// -amount Allocation per key in WHOLE LUX (scaled by 1e6 microLUX internally)
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"github.com/luxfi/constants"
"github.com/luxfi/genesis/pkg/genesis"
)
func main() {
networkID := flag.Uint("network-id", 0, "Network ID (1=mainnet, 2=testnet, 3=devnet, 1337=local)")
numKeys := flag.Int("keys", 1000, "Number of canonical BIP44 wallet keys to fund")
amountLUX := flag.Uint64("amount", 50_000_000, "Allocation per key in WHOLE LUX")
flag.Parse()
if *networkID == 0 {
fmt.Fprintln(os.Stderr, "Error: -network-id is required (1=mainnet, 2=testnet, 3=devnet, 1337=local)")
os.Exit(2)
}
if *numKeys <= 0 {
fmt.Fprintln(os.Stderr, "Error: -keys must be > 0")
os.Exit(2)
}
mnemonic := os.Getenv(genesis.MnemonicEnvVar)
if mnemonic == "" {
fmt.Fprintf(os.Stderr, "Error: %s env var must be set\n", genesis.MnemonicEnvVar)
os.Exit(2)
}
// Derive the canonical BIP44 wallet keys (m/44'/9000'/0'/0/i). This is
// the same call BuildBIP44WalletAllocations uses internally, surfaced as
// KeyInfo so we can feed the canonical ChainAllocations.PChain() path.
keys, err := genesis.LoadBIP44WalletKeysFromMnemonic(mnemonic, *numKeys)
if err != nil {
fmt.Fprintf(os.Stderr, "Error deriving BIP44 wallet keys: %v\n", err)
os.Exit(1)
}
// The 20-byte UTXO/EVM payloads are HRP-independent; the network HRP
// only changes how the utxoAddr is bech32-formatted. constants.GetHRP
// is the single source of truth (mainnet->"lux", testnet->"test", ...).
hrp := constants.GetHRP(uint32(*networkID))
// Convert KeyInfo -> ValidatorKeyInfo so we can reuse the canonical
// PChain() allocation shaper (initialAmount + one locktime-0 unlock),
// which is exactly what the existing genesis files carry.
vkeys := make([]genesis.ValidatorKeyInfo, len(keys))
for i, k := range keys {
vkeys[i] = genesis.ValidatorKeyInfo{
EVMAddr: fmt.Sprintf("0x%s", k.EVMAddr.Hex()),
ShortID: k.StakingAddr,
}
}
// 50_000_000 whole LUX * 1e6 microLUX = 5e13 base units per key — the
// canonical DefaultAllocationPerAccount magnitude under the 6-decimal unit.
amount := *amountLUX * genesis.Lux
allocs, err := genesis.NewAllocations(vkeys, hrp).WithAmount(amount).PChain()
if err != nil {
fmt.Fprintf(os.Stderr, "Error building P-chain allocations: %v\n", err)
os.Exit(1)
}
out, err := json.MarshalIndent(allocs, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Error marshaling allocations: %v\n", err)
os.Exit(1)
}
out = append(out, '\n')
if _, err := os.Stdout.Write(out); err != nil {
fmt.Fprintf(os.Stderr, "Error writing output: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Emitted %d BIP44 allocations for networkID %d (hrp %q), %d LUX each\n",
len(allocs), *networkID, hrp, *amountLUX)
}
+17 -3
View File
@@ -41,7 +41,7 @@ func TestGetGenesis_CChainShardPresentEmbedsCChainGenesis(t *testing.T) {
// TestGetGenesis_XChainShardPresentEmbedsXChainGenesis is the X-Chain
// equivalent of the C-Chain test above. Every Lux network ships an
// xchain.json shard (the small asset descriptor
// `{"symbol":"LUX","name":"Lux","denomination":9}`); the loader must
// `{"symbol":"LUX","name":"Lux","denomination":N}`); the loader must
// thread it into the marshalled primary genesis JSON as the
// xChainGenesis field, where the builder later parses it to construct
// the XVM genesis and CreateChainTx.
@@ -53,7 +53,20 @@ func TestGetGenesis_CChainShardPresentEmbedsCChainGenesis(t *testing.T) {
// hardcoded `Symbol: "LUX", Name: "Lux", Denomination: 9` literal in
// builder.FromConfig is now sourced from the shard, so a P-only
// network (P-only shape) can opt out by simply omitting the file.
//
// Denomination is the X-Chain LUX asset's decimal count and is
// serialized into the asset definition (it feeds AssetIDFromBytes), so
// it is consensus-relevant, not cosmetic. The canonical LUX unit is
// 6 decimals (units.Lux = 1e6 microLUX), so mainnet pins denomination
// 6. testnet/localnet still carry the legacy 9 pending their own
// re-genesis; this test locks the per-network value so any further
// drift is caught.
func TestGetGenesis_XChainShardPresentEmbedsXChainGenesis(t *testing.T) {
wantDenomination := map[string]byte{
"mainnet": 6, // 6-decimal LUX unit (units.Lux = 1e6 microLUX)
"testnet": 9, // legacy; unchanged pending testnet re-genesis
"localnet": 9, // legacy; unchanged pending localnet re-genesis
}
for _, name := range []string{"mainnet", "testnet", "localnet"} {
t.Run(name, func(t *testing.T) {
data, err := GetGenesis(networkIDFromName(t, name))
@@ -77,8 +90,9 @@ func TestGetGenesis_XChainShardPresentEmbedsXChainGenesis(t *testing.T) {
if err := json.Unmarshal([]byte(got), &asset); err != nil {
t.Fatalf("%s: xChainGenesis shard unparseable: %v", name, err)
}
if asset.Symbol != "LUX" || asset.Name != "Lux" || asset.Denomination != 9 {
t.Fatalf("%s: xChainGenesis shard drift: got %+v want {LUX Lux 9}", name, asset)
wantDenom := wantDenomination[name]
if asset.Symbol != "LUX" || asset.Name != "Lux" || asset.Denomination != wantDenom {
t.Fatalf("%s: xChainGenesis shard drift: got %+v want {LUX Lux %d}", name, asset, wantDenom)
}
})
}
+10327 -40027
View File
File diff suppressed because it is too large Load Diff
+10301 -40001
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
{"symbol":"LUX","name":"Lux","denomination":9}
{"symbol":"LUX","name":"Lux","denomination":6}