genesis: shard-driven primary chain set (X opt-in like C/Q/Z)

Make every primary-network chain (X/C/D/Q/A/B/T/Z/G/K) opt-in via its
own <x>chain.json shard. Absent shard -> empty ConfigOutput field ->
builder skips the CreateChainTx -> daemon doesn't start that chain.

Lux mainnet/testnet/devnet ship full chain set as shards; localnet and
local ship the same canonical X asset shard. P-only sovereign L1 boot
shape (<tenant> etc.) is achieved by shipping pchain.json+network.json
only — no env knob, no special builder branch.

X-Chain genesis was previously hardcoded inside FromConfig as a literal
{Symbol:"LUX",Name:"Lux",Denomination:9}. Now sourced from xchain.json
shard, which keeps the same canonical asset for Lux but lets a sibling
network (lqd / <tenant>) ship its own descriptor or ship none and
boot a P-only primary network.

builder.FromConfig:
  - HRP hoisted above the X-Chain conditional (used on both branches).
  - X-Chain construction gated on config.XChainGenesis != "".
  - Returns ids.Empty for xAssetID when X is absent.
  - Single chainEntries table replaces the per-chain switch and the
    DefaultChainGenesis stub-data fallback.
  - "platform" alias renamed to "protocol" (PChainAliases / VMAliases /
    Aliases output) — value is "P-Chain", served by ProtocolVM.

configs.loadAllChainShards / readAllChainShards:
  - Single dispatch over primaryChainShardFiles in canonical order
    (X,C,D,Q,A,B,T,Z,G,K). Adding a new primary chain is one entry +
    one slot, not a new env knob and not a new builder branch.

pkg/genesis/types.go:
  - ConfigOutput.XChainGenesis (omitempty) added; threaded through
    Config <-> ConfigOutput conversions.

Tests:
  - builder/builder_test.go: TestFromConfig_ChainSetIsShardDriven pins
    the contract — full mainnet emits 10 chains + non-empty xAssetID;
    same config with all chain shards stripped emits 0 chains + empty
    xAssetID, validators + UTXOs intact.
  - configs/chain_shards_test.go: TestGetGenesis_XChainShardPresent...
    asserts every Lux network embeds the canonical {LUX,Lux,9} asset;
    AbsentShardEmptyOptIn extended to cover xChainGenesis.
This commit is contained in:
Hanzo AI
2026-05-14 12:32:53 -07:00
parent e50786716c
commit a4b05b40f5
35 changed files with 427 additions and 223 deletions
+2 -2
View File
@@ -111,7 +111,7 @@ Hex: 0x193e5939a08ce9dbd480000000
configs/
├── mainnet/
│ ├── cchain.json # C-Chain genesis (chain ID 96369)
│ ├── pchain.json # P-Chain genesis (Platform)
│ ├── pchain.json # P-Chain genesis (ProtocolVM)
│ ├── network.json # Network ID, message, startTime
│ └── bootstrappers.json # Bootstrap nodes
├── testnet/
@@ -238,7 +238,7 @@ Format: {chain}-{hrp}1{data}{checksum}
Example: P-lux1ck0t9h5u7jvvzhx29n99guqjsfkpzt67wgx7wg
Chain Prefixes:
- P-lux1... (P-Chain/Platform)
- P-lux1... (P-Chain — ProtocolVM)
- X-lux1... (X-Chain/Exchange)
- C-0x... (C-Chain/EVM hex)
```
+1 -1
View File
@@ -8,7 +8,7 @@ This repository contains the canonical genesis configurations for all Lux networ
genesis/
├── mainnet/ # LUX Mainnet (Chain ID: 96369)
│ ├── cchain.json # C-Chain genesis (EVM compatible)
│ ├── pchain.json # P-Chain genesis (Platform chain)
│ ├── pchain.json # P-Chain genesis (ProtocolVM)
│ ├── xchain.json # X-Chain genesis (Exchange chain)
│ ├── network.json # Network configuration
│ └── genesis.json # Full network genesis
+2 -2
View File
@@ -123,7 +123,7 @@ var LocalValidatorFeeConfig fee.Config
```go
var VMAliases = map[ids.ID][]string{
constants.PlatformVMID: {"platform"},
constants.PlatformVMID: {"protocol"}, // upstream constant still PlatformVMID; alias is "protocol"
constants.XVMID: {"xvm"},
constants.EVMID: {"evm"},
secp256k1fx.ID: {"secp256k1fx"},
@@ -155,7 +155,7 @@ func main() {
// Build genesis bytes
config := builder.GetConfig(constants.MainnetID)
genesisBytes, luxAssetID, err := builder.FromConfig(config)
genesisBytes, xAssetID, err := builder.FromConfig(config)
if err != nil {
panic(err)
}
+150 -150
View File
@@ -38,8 +38,10 @@ import (
)
var (
// PChainAliases are the default aliases for the P-Chain
PChainAliases = []string{"P", "platform"}
// PChainAliases are the default aliases for the P-Chain — the
// protocol chain that holds the validator set, chain registry,
// and ordering. Served by ProtocolVM (formerly PlatformVM).
PChainAliases = []string{"P", "protocol"}
// XChainAliases are the default aliases for the X-Chain
XChainAliases = []string{"X", "xvm"}
// CChainAliases are the default aliases for the C-Chain
@@ -61,12 +63,9 @@ var (
// KChainAliases are the default aliases for the K-Chain (KMS)
KChainAliases = []string{"K", "kms", "kmsvm"}
// DefaultChainGenesis provides empty but valid genesis for specialty chains
DefaultChainGenesis = `{"version":1,"message":"Lux Chain Genesis"}`
// VMAliases are the default aliases for VMs
VMAliases = map[ids.ID][]string{
constants.PlatformVMID: {"platform"},
constants.PlatformVMID: {"protocol"},
constants.XVMID: {"xvm"},
constants.EVMID: {"evm"},
constants.DexVMID: {"dexvm", "dex"},
@@ -316,72 +315,107 @@ func GetConfig(networkID uint32) *genesiscfg.Config {
return genesiscfg.GetConfig(networkID)
}
// FromConfig builds genesis bytes from a config
// FromConfig builds genesis bytes from a config.
//
// X-Chain is opt-in via config.XChainGenesis. The shard is a small JSON
// asset descriptor:
//
// {"symbol":"LUX","name":"Lux","denomination":9}
//
// When present, the builder constructs an XVM genesis whose primary
// asset is that descriptor (with initial holders sourced from
// config.Allocations) and emits a CreateChainTx for the X-Chain.
// xAssetID returned to the caller is the X-Chain primary asset ID
// (sha256 over xvmGenesisBytes) — the same ID P-Chain stake UTXOs are
// denominated in.
//
// When absent, no XVM genesis is built, no X-Chain CreateChainTx is
// emitted, and xAssetID is returned as ids.Empty. The primary network
// then has no native UTXO asset: validator stakes denominated against
// ids.Empty have no on-chain asset backing, which is the
// permissioned-set semantics used by L1s whose value capture lives on
// an L2 chain ( etc.) with its own asset model. Minting a
// real primary-network asset requires X — there is no other genesis
// site for it.
func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
// Build XVM (X-Chain) genesis
lux := xgenesis.GenesisAssetDefinition{
Name: "Lux",
Symbol: "LUX",
Denomination: 9,
InitialState: xgenesis.AssetInitialState{},
}
memoBytes := []byte{}
// Sort allocations for deterministic output
type allocation struct {
ETHAddr ids.ShortID
LUXAddr ids.ShortID
InitialAmount uint64
}
xAllocations := []allocation{}
for _, a := range config.Allocations {
if a.InitialAmount > 0 {
xAllocations = append(xAllocations, allocation{
ETHAddr: a.ETHAddr,
LUXAddr: a.LUXAddr,
InitialAmount: a.InitialAmount,
})
}
}
// Get HRP for this network to format bech32 addresses
// HRP is needed for X-Chain holder addresses, P-Chain protocol
// allocations, staker allocations, and reward addresses — define
// once, before the X-Chain conditional, so it's available on both
// paths.
hrp := constants.GetHRP(config.NetworkID)
for _, a := range xAllocations {
// Format address as bech32 for the X-Chain
bech32Addr, err := address.FormatBech32(hrp, a.LUXAddr[:])
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to format bech32 address: %w", err)
}
lux.InitialState.FixedCap = append(lux.InitialState.FixedCap, xgenesis.GenesisHolder{
Amount: a.InitialAmount,
Address: bech32Addr,
})
// Add ETH address to memo for reference
ethAddrStr := a.ETHAddr.Hex()
if len(ethAddrStr) > 2 { // "0x" prefix
memoBytes = append(memoBytes, []byte(ethAddrStr[2:])...)
}
}
lux.Memo = memoBytes
xvmGenesis, err := xgenesis.NewGenesis(
config.NetworkID,
map[string]xgenesis.GenesisAssetDefinition{
"LUX": lux,
},
var (
xvmGenesisBytes []byte
xAssetID = ids.Empty
)
if err != nil {
return nil, ids.Empty, err
}
xvmGenesisBytes, err := xvmGenesis.Bytes()
if err != nil {
return nil, ids.Empty, fmt.Errorf("couldn't serialize xvm genesis: %w", err)
}
if config.XChainGenesis != "" {
var asset struct {
Symbol string `json:"symbol"`
Name string `json:"name"`
Denomination byte `json:"denomination"`
}
if err := json.Unmarshal([]byte(config.XChainGenesis), &asset); err != nil {
return nil, ids.Empty, fmt.Errorf("invalid xChainGenesis shard: %w", err)
}
primary := xgenesis.GenesisAssetDefinition{
Name: asset.Name,
Symbol: asset.Symbol,
Denomination: asset.Denomination,
InitialState: xgenesis.AssetInitialState{},
}
memoBytes := []byte{}
luxAssetID, err := XAssetID(xvmGenesisBytes)
if err != nil {
return nil, ids.Empty, fmt.Errorf("couldn't generate LUX asset ID: %w", err)
// Sort allocations for deterministic output
type allocation struct {
ETHAddr ids.ShortID
LUXAddr ids.ShortID
InitialAmount uint64
}
xAllocations := []allocation{}
for _, a := range config.Allocations {
if a.InitialAmount > 0 {
xAllocations = append(xAllocations, allocation{
ETHAddr: a.ETHAddr,
LUXAddr: a.LUXAddr,
InitialAmount: a.InitialAmount,
})
}
}
for _, a := range xAllocations {
bech32Addr, err := address.FormatBech32(hrp, a.LUXAddr[:])
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to format bech32 address: %w", err)
}
primary.InitialState.FixedCap = append(primary.InitialState.FixedCap, xgenesis.GenesisHolder{
Amount: a.InitialAmount,
Address: bech32Addr,
})
ethAddrStr := a.ETHAddr.Hex()
if len(ethAddrStr) > 2 { // "0x" prefix
memoBytes = append(memoBytes, []byte(ethAddrStr[2:])...)
}
}
primary.Memo = memoBytes
xvmGenesis, err := xgenesis.NewGenesis(
config.NetworkID,
map[string]xgenesis.GenesisAssetDefinition{
asset.Symbol: primary,
},
)
if err != nil {
return nil, ids.Empty, err
}
xvmGenesisBytes, err = xvmGenesis.Bytes()
if err != nil {
return nil, ids.Empty, fmt.Errorf("couldn't serialize xvm genesis: %w", err)
}
xAssetID, err = XAssetID(xvmGenesisBytes)
if err != nil {
return nil, ids.Empty, fmt.Errorf("couldn't generate LUX asset ID: %w", err)
}
}
genesisTime := time.Unix(int64(config.StartTime), 0)
@@ -395,13 +429,13 @@ func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
}
}
// Build platform allocations
// Build protocol allocations
initiallyStaked := set.Set[ids.ShortID]{}
for _, addr := range config.InitialStakedFunds {
initiallyStaked.Add(addr)
}
platformAllocations := []genesis.Allocation{}
protocolAllocations := []genesis.Allocation{}
skippedAllocations := []genesiscfg.Allocation{}
for _, a := range config.Allocations {
if initiallyStaked.Contains(a.LUXAddr) {
@@ -415,7 +449,7 @@ func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to format bech32 address for P-chain: %w", err)
}
platformAllocations = append(platformAllocations, genesis.Allocation{
protocolAllocations = append(protocolAllocations, genesis.Allocation{
Locktime: unlock.Locktime,
Amount: unlock.Amount,
Address: bech32Addr,
@@ -508,87 +542,53 @@ func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
})
}
// Helper to get genesis data or default
getGenesis := func(data string) []byte {
if data == "" {
return []byte(DefaultChainGenesis)
}
return []byte(data)
// Primary-network chain registry. Each entry pairs a genesis-data
// source with the VM that consumes it. Entries with empty
// GenesisData are SKIPPED — luxd will not start a chain manager
// for them. This is the data-driven contract: which primary-network
// chains exist is a function of which shards the operator shipped,
// not a runtime knob.
//
// Order is fixed (X→C→D→Q→A→B→T→Z→G→K) and preserved across
// presence/absence so byte-identical genesis holds when the same
// shard set is supplied. Append-only — reordering shifts the
// P-Chain genesis byte layout.
chainEntries := []struct {
GenesisData []byte
VMID ids.ID
Name string
FxIDs []ids.ID // X-Chain only; nil for everything else
}{
{GenesisData: xvmGenesisBytes, VMID: constants.XVMID, Name: "X-Chain",
FxIDs: []ids.ID{secp256k1fx.ID, nftfx.ID, propertyfx.ID}},
{GenesisData: []byte(config.CChainGenesis), VMID: constants.EVMID, Name: "C-Chain"},
{GenesisData: []byte(config.DChainGenesis), VMID: constants.DexVMID, Name: "D-Chain"},
{GenesisData: []byte(config.QChainGenesis), VMID: constants.QuantumVMID, Name: "Q-Chain"},
{GenesisData: []byte(config.AChainGenesis), VMID: constants.AIVMID, Name: "A-Chain"},
{GenesisData: []byte(config.BChainGenesis), VMID: constants.BridgeVMID, Name: "B-Chain"},
{GenesisData: []byte(config.TChainGenesis), VMID: constants.ThresholdVMID, Name: "T-Chain"},
{GenesisData: []byte(config.ZChainGenesis), VMID: constants.ZKVMID, Name: "Z-Chain"},
{GenesisData: []byte(config.GChainGenesis), VMID: constants.GraphVMID, Name: "G-Chain"},
{GenesisData: []byte(config.KChainGenesis), VMID: constants.KeyVMID, Name: "K-Chain"},
}
// Specify all 11 chains
chains := []genesis.Chain{
{
GenesisData: xvmGenesisBytes,
chains := []genesis.Chain{}
for _, e := range chainEntries {
if len(e.GenesisData) == 0 {
continue
}
chains = append(chains, genesis.Chain{
GenesisData: e.GenesisData,
ChainID: constants.PrimaryNetworkID,
VMID: constants.XVMID,
FxIDs: []ids.ID{
secp256k1fx.ID,
nftfx.ID,
propertyfx.ID,
},
Name: "X-Chain",
},
{
GenesisData: []byte(config.CChainGenesis),
ChainID: constants.PrimaryNetworkID,
VMID: constants.EVMID,
Name: "C-Chain",
},
{
GenesisData: getGenesis(config.DChainGenesis),
ChainID: constants.PrimaryNetworkID,
VMID: constants.DexVMID,
Name: "D-Chain",
},
{
GenesisData: getGenesis(config.QChainGenesis),
ChainID: constants.PrimaryNetworkID,
VMID: constants.QuantumVMID,
Name: "Q-Chain",
},
{
GenesisData: getGenesis(config.AChainGenesis),
ChainID: constants.PrimaryNetworkID,
VMID: constants.AIVMID,
Name: "A-Chain",
},
{
GenesisData: getGenesis(config.BChainGenesis),
ChainID: constants.PrimaryNetworkID,
VMID: constants.BridgeVMID,
Name: "B-Chain",
},
{
GenesisData: getGenesis(config.TChainGenesis),
ChainID: constants.PrimaryNetworkID,
VMID: constants.ThresholdVMID,
Name: "T-Chain",
},
{
GenesisData: getGenesis(config.ZChainGenesis),
ChainID: constants.PrimaryNetworkID,
VMID: constants.ZKVMID,
Name: "Z-Chain",
},
{
GenesisData: getGenesis(config.GChainGenesis),
ChainID: constants.PrimaryNetworkID,
VMID: constants.GraphVMID,
Name: "G-Chain",
},
{
GenesisData: getGenesis(config.KChainGenesis),
ChainID: constants.PrimaryNetworkID,
VMID: constants.KeyVMID,
Name: "K-Chain",
},
VMID: e.VMID,
FxIDs: e.FxIDs,
Name: e.Name,
})
}
pChainGenesis, err := genesis.New(
luxAssetID,
xAssetID,
config.NetworkID,
platformAllocations,
protocolAllocations,
validators,
chains,
config.StartTime,
@@ -596,13 +596,13 @@ func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
config.Message,
)
if err != nil {
return nil, ids.Empty, fmt.Errorf("problem while building platform chain's genesis state: %w", err)
return nil, ids.Empty, fmt.Errorf("problem while building protocol chain's genesis state: %w", err)
}
pChainGenesisBytes, err := pChainGenesis.Bytes()
if err != nil {
return nil, ids.Empty, fmt.Errorf("problem while serializing platform chain's genesis state: %w", err)
return nil, ids.Empty, fmt.Errorf("problem while serializing protocol chain's genesis state: %w", err)
}
return pChainGenesisBytes, luxAssetID, nil
return pChainGenesisBytes, xAssetID, nil
}
// FromFile loads genesis config from file and builds genesis bytes
@@ -685,9 +685,9 @@ func Aliases(genesisBytes []byte) (map[string][]string, map[ids.ID][]string, err
apiAliases := map[string][]string{
path.Join(constants.ChainAliasPrefix, constants.PlatformChainID.String()): {
"P",
"platform",
"protocol",
path.Join(constants.ChainAliasPrefix, "P"),
path.Join(constants.ChainAliasPrefix, "platform"),
path.Join(constants.ChainAliasPrefix, "protocol"),
},
}
chainAliases := map[ids.ID][]string{
+65
View File
@@ -10,6 +10,9 @@ import (
"github.com/stretchr/testify/require"
"github.com/luxfi/constants"
"github.com/luxfi/genesis/configs"
"github.com/luxfi/ids"
pgenesis "github.com/luxfi/protocol/p/genesis"
)
func TestGetStakingConfig(t *testing.T) {
@@ -171,6 +174,68 @@ func TestChainAliases(t *testing.T) {
require.Contains(t, CChainAliases, "C")
}
// TestFromConfig_ChainSetIsShardDriven exercises the data-driven
// chain-presence contract end-to-end:
//
// - Lux mainnet ships every primary-network chain shard
// (X/C/D/Q/A/B/T/Z/G/K) → FromConfig must emit all 10
// CreateChainTx entries and a non-empty xAssetID (X is the home of
// the LUX asset).
//
// - Strip every chain shard from the same Config — same allocations,
// same validators, same network ID — and FromConfig must succeed,
// return ids.Empty for xAssetID, and produce a P-Chain genesis
// with zero CreateChainTx entries. This is the L1-from-P-only
// boot path: a sovereign L1 (lqd-style) ships only pchain.json +
// network.json and gets a primary-network of just P-Chain, with
// funded addresses and weighted validators ready to issue
// CreateChainTx for whatever L2 chains it wants post-genesis.
//
// "Shard set = chain set" is the one-way contract; this test pins it.
func TestFromConfig_ChainSetIsShardDriven(t *testing.T) {
cfg, err := configs.GetConfig(constants.MainnetID)
require.NoError(t, err)
require.NotNil(t, cfg)
require.NotEmpty(t, cfg.XChainGenesis, "mainnet ships xchain.json")
// Full Lux mainnet: 10 chains + real LUX asset ID.
bytes, xAssetID, err := FromConfig(cfg)
require.NoError(t, err)
require.NotEqual(t, ids.Empty, xAssetID, "mainnet has X-Chain → real asset ID")
full, err := pgenesis.Parse(bytes)
require.NoError(t, err)
require.Len(t, full.Chains, 10, "mainnet emits all 10 primary chains")
// Strip every chain shard. Allocations + validators stay intact —
// the P-Chain itself is the foundation, not a CreateChainTx entry.
cfg.XChainGenesis = ""
cfg.CChainGenesis = ""
cfg.DChainGenesis = ""
cfg.QChainGenesis = ""
cfg.AChainGenesis = ""
cfg.BChainGenesis = ""
cfg.TChainGenesis = ""
cfg.ZChainGenesis = ""
cfg.GChainGenesis = ""
cfg.KChainGenesis = ""
bytes, xAssetID, err = FromConfig(cfg)
require.NoError(t, err, "P-only build must succeed")
require.Equal(t, ids.Empty, xAssetID, "no X-Chain → no LUX asset → ids.Empty")
pOnly, err := pgenesis.Parse(bytes)
require.NoError(t, err, "P-only genesis bytes must parse")
require.Empty(t, pOnly.Chains, "P-only network has zero CreateChainTx entries")
// "If wallet/address funded for keys" — the P-Chain still carries
// the Lux mainnet allocations (encoded as P-Chain UTXOs) and
// validator stakes, so a sovereign L1 booted from this shape has
// funded addresses and weighted validators ready to operate.
require.NotEmpty(t, pOnly.Validators, "P-only must have validators (funded staker keys)")
require.NotEmpty(t, pOnly.UTXOs, "P-only must have funded addresses (UTXOs)")
}
func TestDefaultFeeConfigs(t *testing.T) {
// Test dynamic fee configs
configs := []struct {
+1 -1
View File
@@ -44,7 +44,7 @@ func main() {
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)")
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 — matches Lux/Avalanche web wallet & subnet-bootstrap CLIs)")
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)")
format = flag.String("format", "pretty", "Output format: json, pretty")
showHelp = flag.Bool("help", false, "Show help")
+51 -5
View File
@@ -38,6 +38,52 @@ 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
// 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.
//
// Together with TestBuildGenesisFromDir_AbsentShardEmptyOptIn (which
// covers the absent-shard case for X), this enforces the same data-
// driven contract for X that we already have for C/Q/Z: chain
// presence is a file-tree edit, not an operator switch. The previous
// hardcoded `Symbol: "LUX", Name: "Lux", Denomination: 9` literal in
// builder.FromConfig is now sourced from the shard, so a P-only
// network (Liquidity-shape) can opt out by simply omitting the file.
func TestGetGenesis_XChainShardPresentEmbedsXChainGenesis(t *testing.T) {
for _, name := range []string{"mainnet", "testnet", "localnet"} {
t.Run(name, func(t *testing.T) {
data, err := GetGenesis(networkIDFromName(t, name))
if err != nil {
t.Fatalf("GetGenesis(%s): %v", name, err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("parse %s: %v", name, err)
}
got, _ := m["xChainGenesis"].(string)
if got == "" {
t.Fatalf("%s: expected non-empty xChainGenesis (embedded shard present)", name)
}
// Sanity-check the shard parses and pins the canonical Lux asset.
var asset struct {
Symbol string `json:"symbol"`
Name string `json:"name"`
Denomination byte `json:"denomination"`
}
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)
}
})
}
}
// TestBuildGenesisFromDir_AbsentShardEmptyOptIn confirms the FS-fallback
// loader honours the same "shard absent → opt-out" semantics as the
// embedded loader. An operator who wants a P+X-only network just omits
@@ -50,10 +96,10 @@ func TestBuildGenesisFromDir_AbsentShardEmptyOptIn(t *testing.T) {
t.Fatalf("write %s: %v", name, err)
}
}
// Minimal shards: network + P-Chain only. No C/Q/Z. This is the
// Liquidity-shape: primary network with P+X, everything else opt-in
// post-genesis via CreateChainTx.
write("network.json", `{"networkID":1337,"startTime":1735689600,"message":"P+X only test"}`)
// Minimal shards: network + P-Chain only. No X/C/Q/Z. This is the
// P-only Liquidity shape — every primary-network chain (X included)
// is opt-in by shard presence.
write("network.json", `{"networkID":1337,"startTime":1735689600,"message":"P-only test"}`)
write("pchain.json", `{
"allocations":[],
"initialStakeDuration":31536000,
@@ -70,7 +116,7 @@ func TestBuildGenesisFromDir_AbsentShardEmptyOptIn(t *testing.T) {
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("parse: %v", err)
}
for _, field := range []string{"cChainGenesis", "qChainGenesis", "zChainGenesis"} {
for _, field := range []string{"xChainGenesis", "cChainGenesis", "qChainGenesis", "zChainGenesis"} {
if got, _ := m[field].(string); got != "" {
t.Fatalf("absent shard should yield empty %s; got %q", field, got)
}
+109 -46
View File
@@ -200,17 +200,11 @@ func loadEmbeddedGenesisWithDynamic(networkName string, dynamicPChain *genesis.P
// is emitted). Shard absent → empty string, builder skips the entry.
// Runtime gate is luxd's --track-chains, not bake-time env knobs.
//
// To run a P+X-only network (Liquidity etc.), ship a config tree
// that omits cchain.json / qchain.json / zchain.json. No knob, no hack.
cchainData, err := loadOptionalChainShard(networkName, "cchain.json")
if err != nil {
return nil, err
}
qchainData, err := loadOptionalChainShard(networkName, "qchain.json")
if err != nil {
return nil, err
}
zchainData, err := loadOptionalChainShard(networkName, "zchain.json")
// To run a P-only network (Liquidity L1 etc.), ship a config tree
// that omits every {x,c,d,q,a,b,t,z,g,k}chain.json. No knob, no
// hack — chain set is purely data-driven by which shards are
// present.
chainShards, err := loadAllChainShards(networkName)
if err != nil {
return nil, err
}
@@ -230,9 +224,16 @@ func loadEmbeddedGenesisWithDynamic(networkName string, dynamicPChain *genesis.P
InitialStakeDurationOffset: pchain.InitialStakeDurationOffset,
InitialStakedFunds: pchain.InitialStakedFunds,
InitialStakers: pchain.InitialStakers,
CChainGenesis: cchainData,
QChainGenesis: qchainData,
ZChainGenesis: zchainData,
XChainGenesis: chainShards.X,
CChainGenesis: chainShards.C,
DChainGenesis: chainShards.D,
QChainGenesis: chainShards.Q,
AChainGenesis: chainShards.A,
BChainGenesis: chainShards.B,
TChainGenesis: chainShards.T,
ZChainGenesis: chainShards.Z,
GChainGenesis: chainShards.G,
KChainGenesis: chainShards.K,
SecurityProfile: securityProfile,
Message: network.Message,
}
@@ -240,14 +241,79 @@ func loadEmbeddedGenesisWithDynamic(networkName string, dynamicPChain *genesis.P
return json.Marshal(config)
}
// chainShardSet is the full primary-network chain shard set. Each
// field is the raw JSON content of the corresponding shard file, or
// "" when the shard is absent. Absent fields produce no chain entry
// in the builder's chains slice — the operator's filesystem is the
// declarative source of truth for which primary-network chains exist.
type chainShardSet struct {
X, C, D, Q, A, B, T, Z, G, K string
}
// primaryChainShardFiles lists every primary-network chain shard
// filename in canonical order. Order matters: builder.FromConfig
// preserves it when assembling the chains slice, so changing this
// list shifts the P-Chain genesis byte layout. Append-only.
var primaryChainShardFiles = [...]string{
"xchain.json",
"cchain.json",
"dchain.json",
"qchain.json",
"achain.json",
"bchain.json",
"tchain.json",
"zchain.json",
"gchain.json",
"kchain.json",
}
// chainShardSetSlots returns the address-of-field slots for s in the
// canonical order of primaryChainShardFiles. Used by the embedded and
// FS loaders to bind shard files to ConfigOutput fields without
// repeating the per-chain switch.
func (s *chainShardSet) slots() []*string {
return []*string{&s.X, &s.C, &s.D, &s.Q, &s.A, &s.B, &s.T, &s.Z, &s.G, &s.K}
}
// loadAllChainShards reads every primary-network chain shard from the
// embedded tree for networkName. Missing shards become empty fields
// (chain skipped at build time).
func loadAllChainShards(networkName string) (chainShardSet, error) {
var s chainShardSet
slots := s.slots()
for i, file := range primaryChainShardFiles {
v, err := loadOptionalChainShard(networkName, file)
if err != nil {
return s, err
}
*slots[i] = v
}
return s, nil
}
// readAllChainShards is the FS-backed counterpart used by the on-disk
// fallback loader (~/.lux/genesis/<network>/, etc.).
func readAllChainShards(dir string) (chainShardSet, error) {
var s chainShardSet
slots := s.slots()
for i, file := range primaryChainShardFiles {
v, err := readDirShard(dir, file)
if err != nil {
return s, err
}
*slots[i] = v
}
return s, nil
}
// loadOptionalChainShard reads a per-network chain genesis shard from the
// embedded tree. Returns the file contents on success, "" when the file is
// absent (chain not baked into this network's primary genesis), or an error
// for any other read failure.
//
// This is the single dispatch point for every opt-in primary-network chain
// (C, Q, Z, and any future additions). One pattern, one implementation;
// adding a new chain is one call site in the loader, not a new env knob.
// One pattern, one implementation; adding a new primary-network chain
// is one entry in primaryChainShardFiles plus a slot on chainShardSet,
// not a new env knob and not a new branch in the builder.
func loadOptionalChainShard(networkName, filename string) (string, error) {
data, err := embeddedGenesis.ReadFile(filepath.Join(networkName, filename))
if err != nil {
@@ -419,18 +485,9 @@ func buildCanonicalGenesisFromSplitFiles(networkName string) ([]byte, error) {
return nil, fmt.Errorf("failed to parse pchain.json: %w", err)
}
// Opt-in chains via embedded shards. Absent file → empty string →
// chain skipped by builder. One pattern across every primary-network
// chain.
cchainData, err := loadOptionalChainShard(networkName, "cchain.json")
if err != nil {
return nil, err
}
qchainData, err := loadOptionalChainShard(networkName, "qchain.json")
if err != nil {
return nil, err
}
zchainData, err := loadOptionalChainShard(networkName, "zchain.json")
// Opt-in chains via embedded shards. Same data-driven contract as
// the primary loader: shard present → chain emitted; absent → skipped.
chainShards, err := loadAllChainShards(networkName)
if err != nil {
return nil, err
}
@@ -444,9 +501,16 @@ func buildCanonicalGenesisFromSplitFiles(networkName string) ([]byte, error) {
InitialStakeDurationOffset: pchain.InitialStakeDurationOffset,
InitialStakedFunds: pchain.InitialStakedFunds,
InitialStakers: pchain.InitialStakers,
CChainGenesis: cchainData,
QChainGenesis: qchainData,
ZChainGenesis: zchainData,
XChainGenesis: chainShards.X,
CChainGenesis: chainShards.C,
DChainGenesis: chainShards.D,
QChainGenesis: chainShards.Q,
AChainGenesis: chainShards.A,
BChainGenesis: chainShards.B,
TChainGenesis: chainShards.T,
ZChainGenesis: chainShards.Z,
GChainGenesis: chainShards.G,
KChainGenesis: chainShards.K,
Message: network.Message,
}
@@ -512,17 +576,9 @@ func buildGenesisFromDir(dir string) ([]byte, error) {
return nil, fmt.Errorf("failed to parse pchain.json: %w", err)
}
// Opt-in chains: read each shard from dir. Missing file → empty string →
// builder skips the entry. Same pattern as the embedded loader.
cchainData, err := readDirShard(dir, "cchain.json")
if err != nil {
return nil, err
}
qchainData, err := readDirShard(dir, "qchain.json")
if err != nil {
return nil, err
}
zchainData, err := readDirShard(dir, "zchain.json")
// Opt-in chains: read every shard from dir. Same data-driven
// contract as the embedded loader.
chainShards, err := readAllChainShards(dir)
if err != nil {
return nil, err
}
@@ -552,9 +608,16 @@ func buildGenesisFromDir(dir string) ([]byte, error) {
InitialStakeDurationOffset: pchain.InitialStakeDurationOffset,
InitialStakedFunds: pchain.InitialStakedFunds,
InitialStakers: pchain.InitialStakers,
CChainGenesis: cchainData,
QChainGenesis: qchainData,
ZChainGenesis: zchainData,
XChainGenesis: chainShards.X,
CChainGenesis: chainShards.C,
DChainGenesis: chainShards.D,
QChainGenesis: chainShards.Q,
AChainGenesis: chainShards.A,
BChainGenesis: chainShards.B,
TChainGenesis: chainShards.T,
ZChainGenesis: chainShards.Z,
GChainGenesis: chainShards.G,
KChainGenesis: chainShards.K,
SecurityProfile: securityProfile,
Message: network.Message,
}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"symbol":"LUX","name":"Lux","denomination":9}
+1
View File
@@ -0,0 +1 @@
{"symbol":"LUX","name":"Lux","denomination":9}
+1
View File
@@ -0,0 +1 @@
{"symbol":"LUX","name":"Lux","denomination":9}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"symbol":"LUX","name":"Lux","denomination":9}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"version":1,"message":"Lux Chain Genesis"}
+1
View File
@@ -0,0 +1 @@
{"symbol":"LUX","name":"Lux","denomination":9}
+6 -6
View File
@@ -14,7 +14,7 @@ import (
type ChainRole string
const (
RolePlatform ChainRole = "P" // Platform/staking chain
RoleProtocol ChainRole = "P" // Protocol chain — validator set, chain registry, ordering
RoleExchange ChainRole = "X" // Exchange/asset transfer chain
RoleContract ChainRole = "C" // Contract/EVM chain
RoleQuantum ChainRole = "Q" // Quantum-resistant chain
@@ -185,12 +185,12 @@ func DefaultMainnetMapping() *ChainMapping {
return &ChainMapping{
Version: 1,
Chains: map[ChainRole]*ChainConfig{
RolePlatform: {
RoleProtocol: {
ChainID: ids.PChainID,
VMID: ids.Empty, // Platform VM ID
Role: RolePlatform,
Name: "Platform Chain",
Aliases: []string{"P", "platform"},
VMID: ids.Empty, // ProtocolVM ID
Role: RoleProtocol,
Name: "P-Chain",
Aliases: []string{"P", "protocol"},
},
RoleExchange: {
ChainID: ids.XChainID,
+3
View File
@@ -98,6 +98,7 @@ func GetConfigFile(filepath string) (*Config, error) {
InitialStakeDurationOffset: output.InitialStakeDurationOffset,
InitialStakedFunds: stakedFunds,
InitialStakers: stakers,
XChainGenesis: output.XChainGenesis,
CChainGenesis: output.CChainGenesis,
DChainGenesis: output.DChainGenesis,
QChainGenesis: output.QChainGenesis,
@@ -302,9 +303,11 @@ func ParseConfigOutput(output *ConfigOutput, networkID uint32) (*Config, error)
InitialStakeDurationOffset: output.InitialStakeDurationOffset,
InitialStakedFunds: stakedFunds,
InitialStakers: stakers,
XChainGenesis: output.XChainGenesis,
CChainGenesis: output.CChainGenesis,
DChainGenesis: output.DChainGenesis,
QChainGenesis: output.QChainGenesis,
AChainGenesis: output.AChainGenesis,
BChainGenesis: output.BChainGenesis,
TChainGenesis: output.TChainGenesis,
ZChainGenesis: output.ZChainGenesis,
+8 -7
View File
@@ -890,9 +890,10 @@ func BuildWalletAllocations(nid uint32, numKeys int, amountPerKey uint64) ([]All
// BuildBIP44WalletAllocations derives wallet keys on the canonical BIP44
// path m/44'/9000'/0'/0/i (purpose-44' / coin-9000' / account-0' hardened;
// change-0 / index-i NON-hardened). This is the path the Lux/Avalanche
// web wallet and any BIP44-conformant client uses. Returns free (no
// vesting) spending allocations for each key.
// change-0 / index-i NON-hardened). 9000 is the SLIP-0044 coin type Lux
// inherits from its upstream lineage; any BIP44-conformant wallet using
// that coin type derives the same key set. Returns free (no vesting)
// spending allocations for each key.
//
// Use this instead of BuildWalletAllocations when the receiving consumer
// (e.g. a subnet-bootstrap CLI) expects classical BIP44 web-wallet
@@ -1113,8 +1114,8 @@ func buildConfigFromKeyInfos(networkID uint32, validatorKeys []KeyInfo, allKeys
// Track which staking addresses already have an allocation. Validator
// addresses that aren't already in allKeys need their own stake
// allocation; otherwise PlatformVM rejects the genesis with
// "validator has not weight" because there's no UTXO at the
// allocation; otherwise the ProtocolVM rejects the genesis
// with "validator has not weight" because there's no UTXO at the
// validator's stakedFunds address.
allocByAddr := make(map[ids.ShortID]bool, len(allocations))
for _, a := range allocations {
@@ -1122,7 +1123,7 @@ func buildConfigFromKeyInfos(networkID uint32, validatorKeys []KeyInfo, allKeys
}
// stakeAmount is the locktime-locked stake each validator
// contributes. Matches the prior genesis behaviour (3B nLUX across
// three future-locktime buckets), so the PlatformVM sees a
// three future-locktime buckets), so the ProtocolVM sees a
// non-zero stake for each initial staker.
const stakeAmount = uint64(1_000_000_000)
now := uint64(time.Now().Unix())
@@ -1138,7 +1139,7 @@ func buildConfigFromKeyInfos(networkID uint32, validatorKeys []KeyInfo, allKeys
// Ensure the validator's staking address has a corresponding
// allocation. If absent, add one with locked stake so the
// PlatformVM can weight the staker.
// ProtocolVM can weight the staker.
if !allocByAddr[key.StakingAddr] {
allocations = append(allocations, Allocation{
ETHAddr: key.ETHAddr,
+6 -3
View File
@@ -21,6 +21,7 @@ type Config struct {
InitialStakeDurationOffset uint64 `json:"initialStakeDurationOffset"`
InitialStakedFunds []ids.ShortID `json:"initialStakedFunds"`
InitialStakers []Staker `json:"initialStakers"`
XChainGenesis string `json:"xChainGenesis,omitempty"` // X-Chain: UTXO Exchange VM (small JSON: {"symbol":"LUX","name":"Lux","denomination":9}); empty → skip X-Chain
CChainGenesis string `json:"cChainGenesis"`
DChainGenesis string `json:"dChainGenesis,omitempty"` // D-Chain: DEX VM genesis
QChainGenesis string `json:"qChainGenesis,omitempty"` // Q-Chain: Quantum VM genesis
@@ -34,8 +35,8 @@ type Config struct {
// Chains defines additional chains to include in genesis beyond the
// built-in alphabet chains (C, D, Q, B, T, Z, G, K). Each entry becomes
// a CreateChainTx in the platform genesis. This allows L1/subnet chains
// to be embedded directly in the genesis for automatic bootstrap.
// a CreateChainTx in the P-Chain genesis. This allows L1/L2
// chains to be embedded directly for automatic bootstrap.
Chains []ChainEntry `json:"chains,omitempty"`
// ChainMapping provides dynamic chain ID configuration per network.
@@ -244,7 +245,7 @@ type WarpConfig struct {
}
// ChainEntry defines an additional chain to create at genesis.
// Used for embedding L1/L2 chains directly in the platform genesis.
// Used for embedding L1/L2 chains directly in the P-Chain genesis.
type ChainEntry struct {
VMID string `json:"vmID"` // VM ID (base58 encoded)
Name string `json:"name"` // Human-readable chain name
@@ -266,6 +267,7 @@ type ConfigOutput struct {
InitialStakeDurationOffset uint64 `json:"initialStakeDurationOffset"`
InitialStakedFunds []string `json:"initialStakedFunds"`
InitialStakers []StakerJSON `json:"initialStakers"`
XChainGenesis string `json:"xChainGenesis,omitempty"` // X-Chain: UTXO Exchange VM (small JSON: {"symbol":"LUX","name":"Lux","denomination":9}); empty → skip X-Chain
CChainGenesis string `json:"cChainGenesis"`
DChainGenesis string `json:"dChainGenesis,omitempty"` // D-Chain: DEX VM genesis
QChainGenesis string `json:"qChainGenesis,omitempty"` // Q-Chain: Quantum VM genesis
@@ -323,6 +325,7 @@ func (c *Config) ToJSON(hrp string) *ConfigOutput {
InitialStakeDurationOffset: c.InitialStakeDurationOffset,
InitialStakedFunds: stakedFunds,
InitialStakers: stakers,
XChainGenesis: c.XChainGenesis,
CChainGenesis: c.CChainGenesis,
DChainGenesis: c.DChainGenesis,
QChainGenesis: c.QChainGenesis,