Files
zeekayandHanzo Dev e1c1e89412 LUX has six decimals, and mainnet's validator floor is 1,000 LUX
The X-Chain asset declared nine decimals in every network's genesis while the
node's units, the genesis parameters, and every amount on the live P-Chain are
six. Denomination is display metadata — nothing computes with it — so the label
told every wallet and explorer to place the point three places wrong. Six is
also the only precision at which the two-trillion cap fits a uint64; nine
overflows it, which is why six was chosen.

The mainnet validator floor is 1,000 LUX for any network built from this
config. The running chain keeps 2,000 until the node's staking-policy history
moves it, which is that mechanism's job.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-16 11:36:10 -07:00

1071 lines
36 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package builder provides genesis byte generation for Lux networks.
// This package depends on node types and is responsible for building
// the actual genesis state from genesis config.
package builder
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/netip"
"path"
"time"
"github.com/luxfi/address"
"github.com/luxfi/constants"
"github.com/luxfi/container/sampler"
"github.com/luxfi/formatting"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
pchainblock "github.com/luxfi/proto/p/block"
"github.com/luxfi/proto/p/genesis"
"github.com/luxfi/proto/p/reward"
"github.com/luxfi/proto/p/signer"
pchaintxs "github.com/luxfi/proto/p/txs"
"github.com/luxfi/proto/p/validators/fee"
"github.com/luxfi/proto/x/fxs"
xgenesis "github.com/luxfi/proto/x/genesis"
xchaintxs "github.com/luxfi/proto/x/txs"
protozapcodec "github.com/luxfi/proto/zap_codec"
"github.com/luxfi/utxo/nftfx"
"github.com/luxfi/utxo/propertyfx"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/vm/components/gas"
genesiscfg "github.com/luxfi/genesis/pkg/genesis"
)
var (
// 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
CChainAliases = []string{"C", "evm"}
// DChainAliases are the default aliases for the D-Chain (DEX)
DChainAliases = []string{"D", "dex", "dexvm"}
// QChainAliases are the default aliases for the Q-Chain (Quantum)
QChainAliases = []string{"Q", "quantum", "quantumvm"}
// AChainAliases are the default aliases for the A-Chain (AI)
AChainAliases = []string{"A", "ai", "aivm"}
// BChainAliases are the default aliases for the B-Chain (Bridge)
BChainAliases = []string{"B", "bridge", "bridgevm"}
// FChainAliases are the default aliases for the F-Chain (threshold-FHE).
// LP-134: F-Chain runs the ThresholdVM substrate in FHE mode (confidential
// compute / off-EVM threshold DECRYPT). It supersedes the retired T-Chain;
// the legacy "T"/"threshold" labels now belong only to teleportvm
// (LP-6332). On-disk shard: fchain.json.
FChainAliases = []string{"F", "fhe", "fhevm"}
// ZChainAliases are the default aliases for the Z-Chain (ZK)
ZChainAliases = []string{"Z", "zk", "zkvm"}
// GChainAliases are the default aliases for the G-Chain (Graph)
GChainAliases = []string{"G", "graph", "graphvm"}
// KChainAliases are the default aliases for the K-Chain (KMS).
// LP-134: "K stays keyvm" — the threshold-MPC labels M/mpc/mpcvm moved off
// K onto the M-Chain (MChainAliases). K-Chain is pure KMS key management.
KChainAliases = []string{"K", "key", "keyvm", "kms", "kmsvm"}
// MChainAliases are the default aliases for the M-Chain (threshold-MPC).
// LP-134: M-Chain runs the ThresholdVM substrate in MPC mode (CGGMP21 /
// FROST threshold SIGNING for bridge custody). On-disk shard: mchain.json.
MChainAliases = []string{"M", "mpc", "mpcvm"}
// VMAliases are the default aliases for VMs
VMAliases = map[ids.ID][]string{
constants.PlatformVMID: {"protocol"},
constants.XVMID: {"xvm"},
constants.EVMID: {"evm"},
constants.DexVMID: {"dexvm", "dex"},
constants.QuantumVMID: {"quantumvm", "quantum"},
constants.AIVMID: {"aivm", "ai"},
constants.BridgeVMID: {"bridgevm", "bridge"},
constants.MPCVMID: {"mpc", "mpcvm"},
constants.FHEVMID: {"fhe", "fhevm"},
constants.ZKVMID: {"zkvm", "zk"},
constants.GraphVMID: {"graphvm", "graph"},
constants.KeyVMID: {"kmsvm", "kms"},
secp256k1fx.ID: {"secp256k1fx"},
nftfx.ID: {"nftfx"},
propertyfx.ID: {"propertyfx"},
}
errNoTxs = errors.New("genesis creates no transactions")
errOverridesStandardNetworkConfig = errors.New("overrides standard network genesis config")
)
// Bootstrapper represents a network bootstrap node with parsed types
type Bootstrapper struct {
ID ids.NodeID
IP netip.AddrPort
}
// ParseBootstrapper converts a genesis config bootstrapper to a parsed Bootstrapper
func ParseBootstrapper(b genesiscfg.Bootstrapper) (Bootstrapper, error) {
nodeID, err := ids.NodeIDFromString(b.ID)
if err != nil {
return Bootstrapper{}, fmt.Errorf("invalid bootstrapper ID %q: %w", b.ID, err)
}
ip, err := netip.ParseAddrPort(b.IP)
if err != nil {
return Bootstrapper{}, fmt.Errorf("invalid bootstrapper IP %q: %w", b.IP, err)
}
return Bootstrapper{ID: nodeID, IP: ip}, nil
}
// parseProofOfPossession converts a genesis config ProofOfPossession (with hex strings)
// to a node signer.ProofOfPossession (with byte arrays).
func parseProofOfPossession(pop *genesiscfg.ProofOfPossession) (*signer.ProofOfPossession, error) {
if pop == nil {
return nil, nil
}
// Decode the public key from hex string (may have 0x prefix)
pkBytes, err := formatting.Decode(formatting.HexNC, pop.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to decode publicKey: %w", err)
}
// Decode the proof of possession from hex string (may have 0x prefix)
popBytes, err := formatting.Decode(formatting.HexNC, pop.ProofOfPossession)
if err != nil {
return nil, fmt.Errorf("failed to decode proofOfPossession: %w", err)
}
result := &signer.ProofOfPossession{}
copy(result.PublicKey[:], pkBytes)
copy(result.ProofOfPossession[:], popBytes)
// Verify the proof of possession is valid
if err := result.Verify(); err != nil {
return nil, fmt.Errorf("invalid proof of possession: %w", err)
}
return result, nil
}
// GetBootstrappers returns parsed bootstrappers for the network
func GetBootstrappers(networkID uint32) ([]Bootstrapper, error) {
cfgBootstrappers := genesiscfg.GetBootstrappers(networkID)
result := make([]Bootstrapper, 0, len(cfgBootstrappers))
for _, b := range cfgBootstrappers {
parsed, err := ParseBootstrapper(b)
if err != nil {
return nil, err
}
result = append(result, parsed)
}
return result, nil
}
// SampleBootstrappers returns a random sample of bootstrappers for the network
func SampleBootstrappers(networkID uint32, count int) ([]Bootstrapper, error) {
allBootstrappers, err := GetBootstrappers(networkID)
if err != nil {
return nil, err
}
count = min(count, len(allBootstrappers))
if count <= 0 {
return nil, nil
}
s := sampler.NewUniform()
s.Initialize(uint64(len(allBootstrappers)))
indices, _ := s.Sample(count)
sampled := make([]Bootstrapper, 0, len(indices))
for _, index := range indices {
sampled = append(sampled, allBootstrappers[int(index)])
}
return sampled, nil
}
// StakingConfig is the staking configuration with time.Duration types
type StakingConfig struct {
UptimeRequirement float64
MinValidatorStake uint64
MaxValidatorStake uint64
MinDelegatorStake uint64
MinDelegationFee uint32
MinStakeDuration time.Duration
MaxStakeDuration time.Duration
RewardConfig reward.Config
// BLS key information for genesis replay
NodeID string `json:"nodeID"`
BLSPublicKey []byte `json:"blsPublicKey"`
BLSProofOfPossession []byte `json:"blsProofOfPossession"`
}
// GetStakingConfig returns the staking config with time.Duration types
func GetStakingConfig(networkID uint32) StakingConfig {
cfg := genesiscfg.GetStakingConfig(networkID)
return StakingConfig{
UptimeRequirement: cfg.UptimeRequirement,
MinValidatorStake: cfg.MinValidatorStake,
MaxValidatorStake: cfg.MaxValidatorStake,
MinDelegatorStake: cfg.MinDelegatorStake,
MinDelegationFee: cfg.MinDelegationFee,
MinStakeDuration: time.Duration(cfg.MinStakeDuration) * time.Second,
MaxStakeDuration: time.Duration(cfg.MaxStakeDuration) * time.Second,
RewardConfig: reward.Config{
MaxConsumptionRate: cfg.RewardConfig.MaxConsumptionRate,
MinConsumptionRate: cfg.RewardConfig.MinConsumptionRate,
MintingPeriod: time.Duration(cfg.RewardConfig.MintingPeriod) * time.Second,
SupplyCap: cfg.RewardConfig.SupplyCap,
},
}
}
// TxFeeConfig contains transaction fee configuration
// This includes the basic fee config from genesis plus dynamic/validator fees
type TxFeeConfig struct {
TxFee uint64 `json:"txFee"`
CreateAssetTxFee uint64 `json:"createAssetTxFee"`
DynamicFeeConfig gas.Config `json:"dynamicFeeConfig"`
ValidatorFeeConfig fee.Config `json:"validatorFeeConfig"`
}
// Default dynamic fee parameters
var (
MainnetDynamicFeeConfig = gas.Config{
Weights: gas.Dimensions{
gas.Bandwidth: 1,
gas.DBRead: 1,
gas.DBWrite: 1,
gas.Compute: 1,
},
MaxCapacity: 1_000_000,
MaxPerSecond: 100_000,
TargetPerSecond: 50_000,
MinPrice: 1,
ExcessConversionConstant: 5_000,
}
TestnetDynamicFeeConfig = gas.Config{
Weights: gas.Dimensions{
gas.Bandwidth: 1,
gas.DBRead: 1,
gas.DBWrite: 1,
gas.Compute: 1,
},
MaxCapacity: 1_000_000,
MaxPerSecond: 100_000,
TargetPerSecond: 50_000,
MinPrice: 1,
ExcessConversionConstant: 5_000,
}
LocalDynamicFeeConfig = gas.Config{
Weights: gas.Dimensions{
gas.Bandwidth: 1,
gas.DBRead: 1,
gas.DBWrite: 1,
gas.Compute: 1,
},
MaxCapacity: 1_000_000,
MaxPerSecond: 100_000,
TargetPerSecond: 50_000,
MinPrice: 1,
ExcessConversionConstant: 5_000,
}
MainnetValidatorFeeConfig = fee.Config{
Capacity: 20_000,
Target: 10_000,
MinPrice: 512,
ExcessConversionConstant: 1_587,
}
TestnetValidatorFeeConfig = fee.Config{
Capacity: 20_000,
Target: 10_000,
MinPrice: 512,
ExcessConversionConstant: 1_587,
}
LocalValidatorFeeConfig = fee.Config{
Capacity: 20_000,
Target: 10_000,
MinPrice: 512,
ExcessConversionConstant: 1_587,
}
)
// GetTxFeeConfig returns the tx fee config
func GetTxFeeConfig(networkID uint32) TxFeeConfig {
cfg := genesiscfg.GetTxFeeConfig(networkID)
var dynamicCfg gas.Config
var validatorCfg fee.Config
switch networkID {
case constants.MainnetID:
dynamicCfg = MainnetDynamicFeeConfig
validatorCfg = MainnetValidatorFeeConfig
case constants.TestnetID:
dynamicCfg = TestnetDynamicFeeConfig
validatorCfg = TestnetValidatorFeeConfig
default:
dynamicCfg = LocalDynamicFeeConfig
validatorCfg = LocalValidatorFeeConfig
}
return TxFeeConfig{
TxFee: cfg.TxFee,
CreateAssetTxFee: cfg.CreateAssetTxFee,
DynamicFeeConfig: dynamicCfg,
ValidatorFeeConfig: validatorCfg,
}
}
// GetConfig returns the genesis config for the given network ID
func GetConfig(networkID uint32) *genesiscfg.Config {
return genesiscfg.GetConfig(networkID)
}
// 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":6}
//
// 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.
// utxoAssetID 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 utxoAssetID 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 primary networks whose value capture
// lives on a downstream chain (EVM 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) {
// 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)
var (
xvmGenesisBytes []byte
utxoAssetID = ids.Empty
)
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{}
// 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 {
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, xgenesis.GenesisHolder{
Amount: a.InitialAmount,
Address: bech32Addr,
})
evmAddrStr := a.EVMAddr.Hex()
if len(evmAddrStr) > 2 { // "0x" prefix
memoBytes = append(memoBytes, []byte(evmAddrStr[2:])...)
}
}
primary.Memo = memoBytes
xvmCodecs, err := newXVMParserCodecs()
if err != nil {
return nil, ids.Empty, err
}
// NewParser registers the canonical XVM tx types
// (BaseTx/CreateAsset/Operation/Import/Export) plus the fx-owned
// payload types (secp256k1fx) onto xvmCodecs.GenesisRegistry —
// without this, the GenesisCodec can't serialize TransferOutput
// and the like. proto/x stayed Codec-free after Wave 1A so the
// SDK owns type registration; we replicate that contract here.
xvmParser, err := xchaintxs.NewParser(xvmCodecs, []fxs.Fx{
&secp256k1fx.Fx{},
})
if err != nil {
return nil, ids.Empty, fmt.Errorf("couldn't construct xvm parser: %w", err)
}
xvmGenesisCodec := xvmParser.GenesisCodec()
xvmGenesis, err := xgenesis.NewGenesis(
config.NetworkID,
map[string]xgenesis.GenesisAssetDefinition{
asset.Symbol: primary,
},
xvmGenesisCodec,
)
if err != nil {
return nil, ids.Empty, err
}
xvmGenesisBytes, err = xvmGenesis.Bytes(xvmGenesisCodec)
if err != nil {
return nil, ids.Empty, fmt.Errorf("couldn't serialize xvm genesis: %w", err)
}
utxoAssetID, err = UTXOAssetID(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)
// Calculate initial supply
initialSupply := uint64(0)
for _, a := range config.Allocations {
initialSupply += a.InitialAmount
for _, unlock := range a.UnlockSchedule {
initialSupply += unlock.Amount
}
}
// Build protocol allocations
initiallyStaked := set.Set[ids.ShortID]{}
for _, addr := range config.InitialStakedFunds {
initiallyStaked.Add(addr)
}
protocolAllocations := []genesis.Allocation{}
skippedAllocations := []genesiscfg.Allocation{}
for _, a := range config.Allocations {
if initiallyStaked.Contains(a.UTXOAddr) {
skippedAllocations = append(skippedAllocations, a)
continue
}
for _, unlock := range a.UnlockSchedule {
if unlock.Amount > 0 {
// Format address as bech32 for the P-Chain
bech32Addr, err := address.FormatBech32(hrp, a.UTXOAddr[:])
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to format bech32 address for P-chain: %w", err)
}
protocolAllocations = append(protocolAllocations, genesis.Allocation{
Locktime: unlock.Locktime,
Amount: unlock.Amount,
Address: bech32Addr,
Message: a.EVMAddr.Bytes(),
})
}
}
}
// Build validators
validators := []genesis.PermissionlessValidator{}
allNodeAllocations := splitAllocations(skippedAllocations, len(config.InitialStakers))
endStakingTime := genesisTime.Add(time.Duration(config.InitialStakeDuration) * time.Second)
stakingOffset := time.Duration(0)
for i, staker := range config.InitialStakers {
// Safely get node allocations (may be empty if no initialStakedFunds)
var nodeAllocations []genesiscfg.Allocation
if i < len(allNodeAllocations) {
nodeAllocations = allNodeAllocations[i]
}
// Use explicit staker times if provided, otherwise use calculated values
startTime := uint64(genesisTime.Unix())
if staker.StartTime > 0 {
startTime = staker.StartTime
}
endTime := endStakingTime.Add(-stakingOffset)
stakingOffset += time.Duration(config.InitialStakeDurationOffset) * time.Second
endTimeUnix := uint64(endTime.Unix())
if staker.EndTime > 0 {
endTimeUnix = staker.EndTime
}
allocations := []genesis.Allocation{}
for _, a := range nodeAllocations {
for _, unlock := range a.UnlockSchedule {
// Format address as bech32 for staker allocations
bech32Addr, err := address.FormatBech32(hrp, a.UTXOAddr[:])
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to format bech32 address for staker allocation: %w", err)
}
allocations = append(allocations, genesis.Allocation{
Locktime: unlock.Locktime,
Amount: unlock.Amount,
Address: bech32Addr,
Message: a.EVMAddr.Bytes(),
})
}
}
// Calculate weight from allocations or use explicit weight
weight := staker.Weight
if weight == 0 {
for _, a := range allocations {
weight += a.Amount
}
}
// Format reward address as bech32
rewardBech32, err := address.FormatBech32(hrp, staker.RewardAddress[:])
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to format bech32 reward address: %w", err)
}
// Parse the BLS proof of possession if present
var blsSigner *signer.ProofOfPossession
if staker.Signer != nil {
blsSigner, err = parseProofOfPossession(staker.Signer)
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to parse proof of possession for staker %d: %w", i, err)
}
}
validators = append(validators, genesis.PermissionlessValidator{
Validator: genesis.Validator{
StartTime: startTime,
EndTime: endTimeUnix,
Weight: weight,
NodeID: staker.NodeID,
},
RewardOwner: &genesis.Owner{
Threshold: 1,
Addresses: []string{rewardBech32},
},
Staked: allocations,
ExactDelegationFee: staker.DelegationFee,
Signer: blsSigner,
})
}
// 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→F→Z→G→K→M) 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.
//
// LP-0130: F-Chain (FHEVMID) and M-Chain (MPCVMID) run distinct VMs.
// The legacy shared substrate is retired; F occupies the retired
// T slot for byte-order stability, M is appended.
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.FChainGenesis), VMID: constants.FHEVMID, Name: "F-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"},
{GenesisData: []byte(config.MChainGenesis), VMID: constants.MPCVMID, Name: "M-Chain"},
}
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: e.VMID,
FxIDs: e.FxIDs,
Name: e.Name,
})
}
pgc, err := pvmGenesisCodec()
if err != nil {
return nil, ids.Empty, fmt.Errorf("problem while constructing protocol chain's genesis codec: %w", err)
}
pChainGenesis, err := genesis.New(
pgc,
utxoAssetID,
config.NetworkID,
protocolAllocations,
validators,
chains,
config.StartTime,
initialSupply,
config.Message,
)
if err != nil {
return nil, ids.Empty, fmt.Errorf("problem while building protocol chain's genesis state: %w", err)
}
pChainGenesisBytes, err := pChainGenesis.Bytes(pgc)
if err != nil {
return nil, ids.Empty, fmt.Errorf("problem while serializing protocol chain's genesis state: %w", err)
}
return pChainGenesisBytes, utxoAssetID, nil
}
// FromFile loads genesis config from file and builds genesis bytes
func FromFile(networkID uint32, filepath string, stakingCfg *StakingConfig, allowCustomGenesis bool) ([]byte, ids.ID, error) {
// Protect standard networks from custom genesis unless explicitly allowed
if !allowCustomGenesis {
switch networkID {
case constants.MainnetID, constants.TestnetID:
return nil, ids.Empty, fmt.Errorf(
"%w: %s",
errOverridesStandardNetworkConfig,
constants.NetworkName(networkID),
)
}
}
config, err := genesiscfg.GetConfigFile(filepath)
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to load genesis file %s: %w", filepath, err)
}
if err := validateConfig(networkID, config, stakingCfg); err != nil {
return nil, ids.Empty, fmt.Errorf("genesis config validation failed: %w", err)
}
return FromConfig(config)
}
// FromFlag parses base64-encoded genesis content and builds genesis bytes
func FromFlag(networkID uint32, genesisContent string, stakingCfg *StakingConfig, allowCustomGenesis bool) ([]byte, ids.ID, error) {
// Protect standard networks from custom genesis unless explicitly allowed
if !allowCustomGenesis {
switch networkID {
case constants.MainnetID, constants.TestnetID:
return nil, ids.Empty, fmt.Errorf(
"%w: %s",
errOverridesStandardNetworkConfig,
constants.NetworkName(networkID),
)
}
}
data, err := base64.StdEncoding.DecodeString(genesisContent)
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to decode base64 genesis content: %w", err)
}
var config genesiscfg.Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, ids.Empty, fmt.Errorf("failed to parse genesis config: %w", err)
}
if err := validateConfig(networkID, &config, stakingCfg); err != nil {
return nil, ids.Empty, fmt.Errorf("genesis config validation failed: %w", err)
}
return FromConfig(&config)
}
// FromDatabase returns genesis data for database replay mode
func FromDatabase(networkID uint32, dbPath string, dbType string, stakingCfg *StakingConfig) ([]byte, ids.ID, error) {
config := genesiscfg.GetConfig(constants.LocalID)
config.NetworkID = networkID
config.Message = "DATABASE_REPLAY_MODE"
return FromConfig(config)
}
// VMGenesis returns the genesis tx for a specific VM
func VMGenesis(genesisBytes []byte, vmID ids.ID) (*pchaintxs.Tx, error) {
pgc, err := pvmGenesisCodec()
if err != nil {
return nil, fmt.Errorf("failed to construct genesis codec: %w", err)
}
gen, err := genesis.Parse(pgc, genesisBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse genesis: %w", err)
}
for _, chain := range gen.Chains {
uChain := chain.Unsigned.(*pchaintxs.CreateChainTx)
if uChain.VMID == vmID {
return chain, nil
}
}
return nil, fmt.Errorf("couldn't find blockchain with VM ID %s", vmID)
}
// Aliases returns the default aliases for chains and APIs
func Aliases(genesisBytes []byte) (map[string][]string, map[ids.ID][]string, error) {
apiAliases := map[string][]string{
path.Join(constants.ChainAliasPrefix, constants.PlatformChainID.String()): {
"P",
"protocol",
path.Join(constants.ChainAliasPrefix, "P"),
path.Join(constants.ChainAliasPrefix, "protocol"),
},
}
chainAliases := map[ids.ID][]string{
constants.PlatformChainID: PChainAliases,
}
pgc, err := pvmGenesisCodec()
if err != nil {
return nil, nil, fmt.Errorf("failed to construct genesis codec: %w", err)
}
gen, err := genesis.Parse(pgc, genesisBytes)
if err != nil {
return nil, nil, err
}
for _, chain := range gen.Chains {
uChain := chain.Unsigned.(*pchaintxs.CreateChainTx)
chainID := chain.ID()
endpoint := path.Join(constants.ChainAliasPrefix, chainID.String())
switch uChain.VMID {
case constants.XVMID:
apiAliases[endpoint] = []string{
"X", "xvm",
path.Join(constants.ChainAliasPrefix, "X"),
path.Join(constants.ChainAliasPrefix, "xvm"),
}
chainAliases[chainID] = XChainAliases
case constants.EVMID:
apiAliases[endpoint] = []string{
"C", "evm",
path.Join(constants.ChainAliasPrefix, "C"),
path.Join(constants.ChainAliasPrefix, "evm"),
}
chainAliases[chainID] = CChainAliases
case constants.DexVMID:
apiAliases[endpoint] = []string{
"D", "dex", "dexvm",
path.Join(constants.ChainAliasPrefix, "D"),
path.Join(constants.ChainAliasPrefix, "dex"),
path.Join(constants.ChainAliasPrefix, "dexvm"),
}
chainAliases[chainID] = DChainAliases
case constants.QuantumVMID:
apiAliases[endpoint] = []string{
"Q", "quantum", "quantumvm",
path.Join(constants.ChainAliasPrefix, "Q"),
path.Join(constants.ChainAliasPrefix, "quantum"),
}
chainAliases[chainID] = QChainAliases
case constants.AIVMID:
apiAliases[endpoint] = []string{
"A", "ai", "aivm",
path.Join(constants.ChainAliasPrefix, "A"),
path.Join(constants.ChainAliasPrefix, "ai"),
}
chainAliases[chainID] = AChainAliases
case constants.BridgeVMID:
apiAliases[endpoint] = []string{
"B", "bridge", "bridgevm",
path.Join(constants.ChainAliasPrefix, "B"),
path.Join(constants.ChainAliasPrefix, "bridge"),
}
chainAliases[chainID] = BChainAliases
case constants.MPCVMID:
apiAliases[endpoint] = []string{
"M", "mpc", "mpcvm",
path.Join(constants.ChainAliasPrefix, "M"),
path.Join(constants.ChainAliasPrefix, "mpc"),
}
chainAliases[chainID] = MChainAliases
case constants.FHEVMID:
apiAliases[endpoint] = []string{
"F", "fhe", "fhevm",
path.Join(constants.ChainAliasPrefix, "F"),
path.Join(constants.ChainAliasPrefix, "fhe"),
}
chainAliases[chainID] = FChainAliases
case constants.ZKVMID:
apiAliases[endpoint] = []string{
"Z", "zk", "zkvm",
path.Join(constants.ChainAliasPrefix, "Z"),
path.Join(constants.ChainAliasPrefix, "zk"),
}
chainAliases[chainID] = ZChainAliases
case constants.GraphVMID:
apiAliases[endpoint] = []string{
"G", "graph", "graphvm",
path.Join(constants.ChainAliasPrefix, "G"),
path.Join(constants.ChainAliasPrefix, "graph"),
}
chainAliases[chainID] = GChainAliases
case constants.KeyVMID:
apiAliases[endpoint] = []string{
"K", "key", "keyvm", "kms",
path.Join(constants.ChainAliasPrefix, "K"),
path.Join(constants.ChainAliasPrefix, "key"),
}
chainAliases[chainID] = KChainAliases
}
}
return apiAliases, chainAliases, nil
}
// pvmGenesisCodec constructs the ZAP-native proto/p genesis codec via
// the canonical proto/zap_codec.NewPVMGenesis constructor. proto/p
// carries no github.com/luxfi/codec import after the Wave 2A rip;
// genesis.New / Genesis.Bytes / genesis.Parse all require the
// genesis-sized codec (math.MaxInt32 budget) because the PVM genesis
// blob can be significantly larger than runtime txs — the full set of
// initial validator stake txs and CreateChainTx entries for every
// primary-network chain lives in a single blob.
//
// block.RegisterTypes pulls in the full Apricot/Banff/Durango/Quasar
// block + tx type set in the canonical wire-byte order required by
// proto/p (block.RegisterTypes is a superset of txs.RegisterTypes —
// see proto/p/block/codec.go).
//
// Wave 2G-Archive (#101): the local staging shim that previously
// mirrored proto/zap_codec's surface has been deleted; this function
// now imports the canonical constructor directly. The returned codec
// is ZAP-native little-endian — forward-only, no rollback path,
// aligned with LP-023 ZAP-native activation (ZAPActivationUnix=0).
//
// Mirrors sdk/wallet/chain/p/pcodecs.NewPVMGenesisCodec.
func pvmGenesisCodec() (pchaintxs.Codec, error) {
cm := protozapcodec.NewPVMGenesis(pchaintxs.CodecVersion)
if err := pchainblock.RegisterTypes(cm); err != nil {
return nil, err
}
return cm, nil
}
// newXVMParserCodecs constructs the ZAP-native ParserCodecs for proto/x
// via the canonical proto/zap_codec.NewXVMParser constructor.
//
// Mirrors sdk/wallet/chain/x/constants.go::newXVMParserCodecs — both
// thread the same ZAP-native zapcodec backend. proto/x carries no
// github.com/luxfi/codec import after the Wave 1A rip; the runtime
// codec is 1 MiB-bounded and the genesis codec is math.MaxInt32-
// bounded — both budgets are baked into NewXVMParser.
//
// Tx-level and fx-owned wire payload types are registered when this
// bundle is handed to xchaintxs.NewParser — see proto/x/txs/parser.go
// (fxOwnedTypes).
func newXVMParserCodecs() (xchaintxs.ParserCodecs, error) {
runtime, genesis := protozapcodec.NewXVMParser(xchaintxs.CodecVersion)
return xchaintxs.ParserCodecs{
Codec: runtime,
GenesisCodec: genesis,
Registry: runtime,
GenesisRegistry: genesis,
}, nil
}
// UTXOAssetID returns the LUX asset ID from XVM genesis bytes
func UTXOAssetID(xvmGenesisBytes []byte) (ids.ID, error) {
codecs, err := newXVMParserCodecs()
if err != nil {
return ids.Empty, err
}
parser, err := xchaintxs.NewParser(
codecs,
[]fxs.Fx{
&secp256k1fx.Fx{},
},
)
if err != nil {
return ids.Empty, err
}
genesisCodec := parser.GenesisCodec()
gen := xgenesis.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 {
return fmt.Errorf("network ID mismatch: expected %d, got %d", networkID, config.NetworkID)
}
if config.InitialStakeDuration > uint64(stakingCfg.MaxStakeDuration.Seconds()) {
return fmt.Errorf("initial stake duration %d exceeds max %d", config.InitialStakeDuration, uint64(stakingCfg.MaxStakeDuration.Seconds()))
}
return nil
}
// DevModeConfig holds configuration for dev mode genesis
type DevModeConfig struct {
NodeID ids.NodeID // The validator node ID
BLSPublicKey string // BLS public key hex
BLSPopProof string // BLS proof of possession hex
RewardAddress ids.ShortID // Reward/allocation address
CChainGenesis string // C-Chain genesis JSON
StartTime uint64 // Genesis start time (if 0, uses time.Now())
}
// ForDevMode creates a genesis configuration suitable for single-node development mode.
// It creates a single validator with far-future stake time and funds the treasury address.
func ForDevMode(cfg DevModeConfig, stakingCfg *StakingConfig) ([]byte, ids.ID, error) {
// Genesis start time: use provided time or fall back to now
startTime := cfg.StartTime
if startTime == 0 {
startTime = uint64(time.Now().Unix())
}
// Far-future stake duration: 100 years in seconds
// This ensures the validator never expires during development
const hundredYears = 100 * 365 * 24 * 60 * 60
// Create allocation for the reward address
// Initial staked amount: 1B LUX (enough to be a validator)
const oneMillionLUX = 1_000_000_000_000_000 // 1M LUX in nLUX
const oneBillionLUX = 1_000_000_000_000_000_000 // 1B LUX in nLUX
allocation := genesiscfg.Allocation{
EVMAddr: cfg.RewardAddress, // Same as UTXO addr for simplicity
UTXOAddr: cfg.RewardAddress,
InitialAmount: oneMillionLUX, // Initial unlocked amount
UnlockSchedule: []genesiscfg.LockedAmount{
{
Amount: oneBillionLUX, // Staked amount
Locktime: 0, // No lock time
},
},
}
// Create the single staker
var signer *genesiscfg.ProofOfPossession
if cfg.BLSPublicKey != "" && cfg.BLSPopProof != "" {
signer = &genesiscfg.ProofOfPossession{
PublicKey: cfg.BLSPublicKey,
ProofOfPossession: cfg.BLSPopProof,
}
}
staker := genesiscfg.Staker{
NodeID: cfg.NodeID,
RewardAddress: cfg.RewardAddress,
DelegationFee: 1000000, // 100% delegation fee (no delegators in dev mode)
Signer: signer,
Weight: oneBillionLUX,
StartTime: startTime,
EndTime: startTime + hundredYears,
}
// Build the genesis config
config := &genesiscfg.Config{
NetworkID: constants.LocalID,
Allocations: []genesiscfg.Allocation{allocation},
StartTime: startTime,
InitialStakeDuration: hundredYears,
InitialStakeDurationOffset: 0,
InitialStakedFunds: []ids.ShortID{cfg.RewardAddress},
InitialStakers: []genesiscfg.Staker{staker},
CChainGenesis: cfg.CChainGenesis,
Message: "Lux Development Mode Genesis",
}
return FromConfig(config)
}
// splitAllocations splits allocations across multiple stakers
func splitAllocations(allocations []genesiscfg.Allocation, numSplits int) [][]genesiscfg.Allocation {
if numSplits == 0 {
return [][]genesiscfg.Allocation{}
}
totalAmount := uint64(0)
for _, a := range allocations {
for _, unlock := range a.UnlockSchedule {
totalAmount += unlock.Amount
}
}
nodeWeight := totalAmount / uint64(numSplits)
allNodeAllocations := make([][]genesiscfg.Allocation, 0, numSplits)
currentNodeAllocation := []genesiscfg.Allocation{}
currentNodeAmount := uint64(0)
for _, allocation := range allocations {
currentAllocation := allocation
currentAllocation.InitialAmount = 0
currentAllocation.UnlockSchedule = nil
for _, unlock := range allocation.UnlockSchedule {
for currentNodeAmount+unlock.Amount > nodeWeight && len(allNodeAllocations) < numSplits-1 {
amountToAdd := nodeWeight - currentNodeAmount
currentAllocation.UnlockSchedule = append(currentAllocation.UnlockSchedule, genesiscfg.LockedAmount{
Amount: amountToAdd,
Locktime: unlock.Locktime,
})
unlock.Amount -= amountToAdd
currentNodeAllocation = append(currentNodeAllocation, currentAllocation)
allNodeAllocations = append(allNodeAllocations, currentNodeAllocation)
currentNodeAllocation = nil
currentNodeAmount = 0
currentAllocation = allocation
currentAllocation.InitialAmount = 0
currentAllocation.UnlockSchedule = nil
}
if unlock.Amount == 0 {
continue
}
currentAllocation.UnlockSchedule = append(currentAllocation.UnlockSchedule, genesiscfg.LockedAmount{
Amount: unlock.Amount,
Locktime: unlock.Locktime,
})
currentNodeAmount += unlock.Amount
}
if len(currentAllocation.UnlockSchedule) > 0 {
currentNodeAllocation = append(currentNodeAllocation, currentAllocation)
}
}
return append(allNodeAllocations, currentNodeAllocation)
}