genesis/builder: X-Chain becomes opt-in (decomplect from "always present")

This commit is contained in:
Hanzo AI
2026-05-20 22:08:54 -07:00
parent c8d2baae09
commit 7a379ec68a
2 changed files with 128 additions and 153 deletions
+9 -19
View File
@@ -50,7 +50,6 @@ import (
"github.com/luxfi/node/version"
"github.com/luxfi/node/vms/components/gas"
"github.com/luxfi/node/vms/platformvm/reward"
pchaintxs "github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/node/vms/platformvm/validators/fee"
"github.com/luxfi/node/vms/proposervm"
"github.com/luxfi/timer"
@@ -1965,24 +1964,15 @@ func saveCachedGenesisBytes(cacheFile string, genesisBytes []byte) error {
return os.WriteFile(cacheFile, genesisBytes, 0o600)
}
// extractXAssetID extracts the LUX asset ID from raw platform genesis bytes.
// This is needed when loading raw genesis bytes directly (for snapshot resume)
// to avoid rebuilding genesis which causes hash mismatch.
func extractXAssetID(genesisBytes []byte) (ids.ID, error) {
// Get the X-chain creation TX from the platform genesis
xChainTx, err := builder.VMGenesis(genesisBytes, constants.XVMID)
if err != nil {
return ids.Empty, fmt.Errorf("couldn't find X-chain genesis in platform genesis: %w", err)
}
// Extract the XVM genesis bytes from the create chain TX
createChainTx, ok := xChainTx.Unsigned.(*pchaintxs.CreateChainTx)
if !ok {
return ids.Empty, fmt.Errorf("X-chain genesis TX is not a CreateChainTx")
}
// Use the builder.XAssetID function to extract the asset ID from XVM genesis
return builder.XAssetID(createChainTx.GenesisData)
// extractXAssetID returns the LUX asset ID for the given platform genesis.
//
// The LUX asset ID is a network-wide constant (constants.UTXO_ASSET_ID),
// not derived from X-Chain genesis bytes — so it is identical whether the
// raw genesis includes an X-Chain entry or not. The genesisBytes argument
// is kept so the snapshot-resume path stays a single function call site;
// it is otherwise unused.
func extractXAssetID(_ []byte) (ids.ID, error) {
return constants.UTXO_ASSET_ID, nil
}
func providedFlags(v *viper.Viper) map[string]interface{} {
+119 -134
View File
@@ -23,8 +23,6 @@ import (
"github.com/luxfi/net/endpoints"
"github.com/luxfi/node/vms/components/gas"
exchangevm "github.com/luxfi/node/vms/xvm"
"github.com/luxfi/node/vms/xvm/fxs"
xchaintxs "github.com/luxfi/node/vms/xvm/txs"
"github.com/luxfi/node/vms/platformvm/genesis"
"github.com/luxfi/node/vms/platformvm/reward"
"github.com/luxfi/node/vms/platformvm/signer"
@@ -102,7 +100,6 @@ var (
bls12381fx.ID: {"bls12381fx"},
}
errNoTxs = errors.New("genesis creates no transactions")
errOverridesStandardNetworkConfig = errors.New("overrides standard network genesis config")
)
@@ -353,78 +350,104 @@ 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 (a small JSON descriptor like
//
// {"symbol":"LUX","name":"Lux","denomination":9}
//
// ). When set, the builder constructs an XVM genesis whose primary asset
// is that descriptor (initial holders sourced from config.Allocations) and
// emits an X-Chain entry in the primary-network CreateChainTx set. When
// empty, no XVM genesis is built and X-Chain is omitted from the chain
// set — the path used by P-only L2s whose value capture lives on a
// downstream EVM ( etc.).
//
// The LUX asset ID returned is always the network-wide constant
// (constants.UTXO_ASSET_ID), independent of whether X-Chain is baked.
// This is what decouples the asset from any specific chain's genesis
// bytes — P-Chain stake/UTXO references stay byte-stable across both
// shapes of primary network.
func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
// Build XVM (X-Chain) genesis
lux := exchangevm.GenesisAssetDefinition{
Name: "Lux",
Symbol: "LUX",
Denomination: 9,
InitialState: exchangevm.AssetInitialState{},
}
memoBytes := []byte{}
// Sort allocations for deterministic output
type allocation struct {
EVMAddr ids.ShortID
UTXOAddr ids.ShortID
InitialAmount uint64
}
xAllocations := []allocation{}
for _, a := range config.Allocations {
if a.InitialAmount > 0 {
xAllocations = append(xAllocations, allocation{
EVMAddr: a.EVMAddr,
UTXOAddr: a.UTXOAddr,
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.UTXOAddr[:])
// X-Chain is the first opt-in chain. Build its XVM genesis bytes only
// when config.XChainGenesis is non-empty; otherwise leave the slot
// empty and the optIn loop below skips emitting an X-Chain CreateChainTx.
var xvmGenesisBytes []byte
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 := exchangevm.GenesisAssetDefinition{
Name: asset.Name,
Symbol: asset.Symbol,
Denomination: asset.Denomination,
InitialState: exchangevm.AssetInitialState{},
}
memoBytes := []byte{}
// Sort allocations for deterministic output
type allocation struct {
EVMAddr ids.ShortID
UTXOAddr ids.ShortID
InitialAmount uint64
}
xAllocations := []allocation{}
for _, a := range config.Allocations {
if a.InitialAmount > 0 {
xAllocations = append(xAllocations, allocation{
EVMAddr: a.EVMAddr,
UTXOAddr: a.UTXOAddr,
InitialAmount: a.InitialAmount,
})
}
}
for _, a := range xAllocations {
// Format address as bech32 for the X-Chain
bech32Addr, err := address.FormatBech32(hrp, a.UTXOAddr[:])
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to format bech32 address: %w", err)
}
primary.InitialState.FixedCap = append(primary.InitialState.FixedCap, exchangevm.GenesisHolder{
Amount: a.InitialAmount,
Address: bech32Addr,
})
// Add ETH address to memo for reference
ethAddrStr := a.EVMAddr.Hex()
if len(ethAddrStr) > 2 { // "0x" prefix
memoBytes = append(memoBytes, []byte(ethAddrStr[2:])...)
}
}
primary.Memo = memoBytes
xvmGenesis, err := exchangevm.NewGenesis(
config.NetworkID,
map[string]exchangevm.GenesisAssetDefinition{
asset.Symbol: primary,
},
)
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to format bech32 address: %w", err)
return nil, ids.Empty, err
}
lux.InitialState.FixedCap = append(lux.InitialState.FixedCap, exchangevm.GenesisHolder{
Amount: a.InitialAmount,
Address: bech32Addr,
})
// Add ETH address to memo for reference
ethAddrStr := a.EVMAddr.Hex()
if len(ethAddrStr) > 2 { // "0x" prefix
memoBytes = append(memoBytes, []byte(ethAddrStr[2:])...)
xvmGenesisBytes, err = xvmGenesis.Bytes()
if err != nil {
return nil, ids.Empty, fmt.Errorf("couldn't serialize xvm genesis: %w", err)
}
}
lux.Memo = memoBytes
xvmGenesis, err := exchangevm.NewGenesis(
config.NetworkID,
map[string]exchangevm.GenesisAssetDefinition{
"LUX": lux,
},
)
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)
}
// LUX asset ID is a network-wide constant (constants.UTXO_ASSET_ID), not
// derived from X-Chain genesis bytes. This decouples the asset from the
// X-Chain so X-Chain can be made optional for P-only L2s.
//
// We still build xvmGenesisBytes above so X-Chain (when baked) has a
// consistent initial-supply view, but the asset ID itself is the
// deterministic constant — all chains reference the same ID regardless
// of whether X-Chain is part of the chain set.
_ = xvmGenesisBytes // bytes still used downstream when X-Chain is baked
// LUX asset ID is a network-wide constant (constants.UTXO_ASSET_ID),
// not derived from X-Chain genesis bytes. All chains reference the
// same ID regardless of whether X-Chain is part of the chain set.
xAssetID := constants.UTXO_ASSET_ID
genesisTime := time.Unix(int64(config.StartTime), 0)
@@ -567,29 +590,29 @@ func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
// Primary-network chain set is data-driven.
//
// Mandatory:
// P-Chain — implicit (the platform genesis itself).
// X-Chain — staking and tx-fee UTXOs are anchored here; removing it
// requires a new native fee asset. Always baked.
// P-Chain is implicit (the platform genesis itself). Every other
// primary-network chain — including X-Chain — is opt-in via a
// non-empty genesis string in the resolved Config. Entries with
// empty GenesisData are SKIPPED — luxd will not start a chain
// manager for them. Mainnet/testnet/devnet ship XChainGenesis so
// X-Chain stays baked; P-only L2s (Liquidity etc.) leave it empty
// and the chain is omitted.
//
// Opt-in (every other chain):
// Each XChainGenesis string in the config is the gate. Non-empty →
// the chain is included in the primary-network CreateChainTx set.
// Downstream forks (Liquidity etc.) that only want P+X just leave
// their other genesis fields empty in the config JSON / shard tree;
// no env knob per chain, no compile-time hack.
// Order is fixed (X→C→D→B→T→Q→Z) 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.
//
// Runtime tracking is separate: an operator decides which of the
// baked chains this node actually runs via --track-chains /
// --track-all-chains. Baked-but-untracked chains stay dormant.
chains := []genesis.Chain{}
// X-Chain — staking + LUX asset, always present.
chains = append(chains, genesis.Chain{
GenesisData: xvmGenesisBytes,
ChainID: constants.PrimaryNetworkID,
VMID: constants.XVMID,
FxIDs: []ids.ID{
optIn := []struct {
genesisData []byte
vmID ids.ID
name string
fxIDs []ids.ID // X-Chain only; nil for everything else.
}{
{xvmGenesisBytes, constants.XVMID, "X-Chain", []ids.ID{
secp256k1fx.ID,
nftfx.ID,
propertyfx.ID,
@@ -599,33 +622,24 @@ func FromConfig(config *genesiscfg.Config) ([]byte, ids.ID, error) {
secp256r1fx.ID,
schnorrfx.ID,
bls12381fx.ID,
},
Name: "X-Chain",
})
// Every other chain is opt-in via a non-empty genesis string in the
// resolved Config. Table-driven so adding a new primary-network chain
// is one row, not a new env knob.
optIn := []struct {
genesisData string
vmID ids.ID
name string
}{
{config.CChainGenesis, constants.EVMID, "C-Chain"},
{config.DChainGenesis, constants.DexVMID, "D-Chain"},
{config.BChainGenesis, constants.BridgeVMID, "B-Chain"},
{config.TChainGenesis, constants.ThresholdVMID, "T-Chain"},
{config.QChainGenesis, constants.QuantumVMID, "Q-Chain"},
{config.ZChainGenesis, constants.ZKVMID, "Z-Chain"},
}},
{[]byte(config.CChainGenesis), constants.EVMID, "C-Chain", nil},
{[]byte(config.DChainGenesis), constants.DexVMID, "D-Chain", nil},
{[]byte(config.BChainGenesis), constants.BridgeVMID, "B-Chain", nil},
{[]byte(config.TChainGenesis), constants.ThresholdVMID, "T-Chain", nil},
{[]byte(config.QChainGenesis), constants.QuantumVMID, "Q-Chain", nil},
{[]byte(config.ZChainGenesis), constants.ZKVMID, "Z-Chain", nil},
}
chains := []genesis.Chain{}
for _, c := range optIn {
if c.genesisData == "" {
if len(c.genesisData) == 0 {
continue
}
chains = append(chains, genesis.Chain{
GenesisData: []byte(c.genesisData),
GenesisData: c.genesisData,
ChainID: constants.PrimaryNetworkID,
VMID: c.vmID,
FxIDs: c.fxIDs,
Name: c.name,
})
}
@@ -829,35 +843,6 @@ func Aliases(genesisBytes []byte) (map[string][]string, map[ids.ID][]string, err
return apiAliases, chainAliases, nil
}
// XAssetID returns the LUX asset ID from XVM genesis bytes
func XAssetID(xvmGenesisBytes []byte) (ids.ID, error) {
parser, err := xchaintxs.NewParser(
[]fxs.Fx{
&secp256k1fx.Fx{},
},
)
if err != nil {
return ids.Empty, err
}
genesisCodec := parser.GenesisCodec()
gen := exchangevm.Genesis{}
if _, err := genesisCodec.Unmarshal(xvmGenesisBytes, &gen); err != nil {
return ids.Empty, err
}
if len(gen.Txs) == 0 {
return ids.Empty, errNoTxs
}
genesisTx := gen.Txs[0]
tx := xchaintxs.Tx{Unsigned: &genesisTx.CreateAssetTx}
if err := tx.Initialize(genesisCodec); err != nil {
return ids.Empty, err
}
return tx.ID(), nil
}
// validateConfig validates the genesis config
func validateConfig(networkID uint32, config *genesiscfg.Config, stakingCfg *StakingConfig) error {
if config.NetworkID != networkID {