Files
node/genesis/builder/builder_test.go
T
zeekayandHanzo Dev c5e999487c Green the 8 post-bump test failures (real fixes, no delete-to-hide)
- spending.go: harden spendingTx.Bytes() against nil msg (uninitialized tx no
  longer panics the spend-verification path; production always sets msg).
- config/genesis-builder tests: codec-era Codec.Marshal -> native g.Bytes();
  AddValidator field access -> methods (Weight()/Validator()/StakeOuts());
  drop unused pchaintxs imports.
- config test: consensus params K=30 alpha 16->20/25 to satisfy v1.36's tighter
  BFT bound (2*alpha-K >= floor((K-1)/3)+1); assertions updated.
- version/compatibility.json: register Current v1.32.11 under RPCChainVM proto 42
  (pre-existing registration gap).
- cevm e2e: genesis fixture completed with gasLimit+difficulty (strict geth
  genesis validation in the installed CEvm plugin; pre-existing fixture drift).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 21:20:17 -07:00

527 lines
16 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package builder
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/platformvm/genesis"
"github.com/luxfi/node/vms/platformvm/stakeable"
"github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/utxo/secp256k1fx"
genesiscfg "github.com/luxfi/genesis/pkg/genesis"
)
func TestGetStakingConfig(t *testing.T) {
tests := []struct {
name string
networkID uint32
}{
{"Mainnet", constants.MainnetID},
{"Testnet", constants.TestnetID},
{"CustomID", constants.LocalID},
{"Custom", 12345},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := GetStakingConfig(tt.networkID)
// Verify basic constraints
require.GreaterOrEqual(t, cfg.UptimeRequirement, 0.0)
require.LessOrEqual(t, cfg.UptimeRequirement, 1.0)
require.Greater(t, cfg.MinValidatorStake, uint64(0))
require.GreaterOrEqual(t, cfg.MaxValidatorStake, cfg.MinValidatorStake)
require.Greater(t, cfg.MinDelegatorStake, uint64(0))
require.Greater(t, cfg.MinStakeDuration, time.Duration(0))
require.GreaterOrEqual(t, cfg.MaxStakeDuration, cfg.MinStakeDuration)
// RewardConfig is populated by builder with node-specific types
// The genesis package only provides the base staking parameters
})
}
}
func TestGetTxFeeConfig(t *testing.T) {
tests := []struct {
name string
networkID uint32
}{
{"Mainnet", constants.MainnetID},
{"Testnet", constants.TestnetID},
{"CustomID", constants.LocalID},
{"Custom", 12345},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := GetTxFeeConfig(tt.networkID)
// Verify basic fee config
require.Greater(t, cfg.TxFee, uint64(0))
require.Greater(t, cfg.CreateAssetTxFee, uint64(0))
// Verify dynamic fee config
require.Greater(t, uint64(cfg.DynamicFeeConfig.MaxCapacity), uint64(0))
require.Greater(t, uint64(cfg.DynamicFeeConfig.MaxPerSecond), uint64(0))
// Verify validator fee config
require.Greater(t, uint64(cfg.ValidatorFeeConfig.Capacity), uint64(0))
require.Greater(t, uint64(cfg.ValidatorFeeConfig.Target), uint64(0))
})
}
}
func TestGetBootstrappers(t *testing.T) {
tests := []struct {
name string
networkID uint32
}{
{"Mainnet", constants.MainnetID},
{"Testnet", constants.TestnetID},
{"Devnet", constants.DevnetID},
{"CustomID", constants.LocalID},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bootstrappers, err := GetBootstrappers(tt.networkID)
require.NoError(t, err)
// Bootstrappers may not be configured for all networks
// This is acceptable - they can be provided via config
// Verify each bootstrapper has valid ID and endpoint
for _, b := range bootstrappers {
require.NotEqual(t, b.ID.String(), "")
require.True(t, b.Endpoint.Port > 0, "endpoint port must be non-zero")
}
})
}
}
func TestSampleBootstrappers(t *testing.T) {
tests := []struct {
name string
networkID uint32
count int
}{
{"Mainnet_5", constants.MainnetID, 5},
{"Mainnet_10", constants.MainnetID, 10},
{"Testnet_3", constants.TestnetID, 3},
{"Custom_0", constants.LocalID, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sampled, err := SampleBootstrappers(tt.networkID, tt.count)
require.NoError(t, err)
// Should not exceed requested count
require.LessOrEqual(t, len(sampled), tt.count)
// Should not exceed available bootstrappers
all, err := GetBootstrappers(tt.networkID)
require.NoError(t, err)
require.LessOrEqual(t, len(sampled), len(all))
})
}
}
func TestGetConfig(t *testing.T) {
tests := []struct {
name string
networkID uint32
}{
{"Mainnet", constants.MainnetID},
{"Testnet", constants.TestnetID},
{"MainnetChainID", constants.MainnetChainID},
{"TestnetChainID", constants.TestnetChainID},
{"CustomID", constants.LocalID},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := GetConfig(tt.networkID)
require.NotNil(t, cfg)
require.Greater(t, cfg.NetworkID, uint32(0))
})
}
}
func TestGetConfigAllocations(t *testing.T) {
tests := []struct {
name string
networkID uint32
minAllocs int
minStakers int
}{
{"Mainnet", constants.MainnetID, 50, 1},
{"Testnet", constants.TestnetID, 50, 1},
{"Devnet", constants.DevnetID, 50, 1},
{"Local", constants.LocalID, 50, 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := GetConfig(tt.networkID)
require.NotNil(t, cfg)
require.GreaterOrEqual(t, len(cfg.Allocations), tt.minAllocs,
"network %d must have at least %d allocations", tt.networkID, tt.minAllocs)
require.GreaterOrEqual(t, len(cfg.InitialStakers), tt.minStakers,
"network %d must have at least %d stakers", tt.networkID, tt.minStakers)
})
}
}
func TestFromConfigNonZeroSupply(t *testing.T) {
for _, networkID := range []uint32{constants.MainnetID, constants.TestnetID, constants.DevnetID, constants.LocalID} {
t.Run(fmt.Sprintf("network_%d", networkID), func(t *testing.T) {
cfg := GetConfig(networkID)
require.NotNil(t, cfg)
genesisBytes, _, err := FromConfig(cfg)
require.NoError(t, err)
require.NotEmpty(t, genesisBytes)
})
}
}
func TestVMAliases(t *testing.T) {
// Verify all expected VMs have aliases
require.Contains(t, VMAliases, constants.PlatformVMID)
require.Contains(t, VMAliases, constants.XVMID)
require.Contains(t, VMAliases, constants.EVMID)
// Verify aliases are non-empty
for vmID, aliases := range VMAliases {
require.NotEmpty(t, aliases, "VM %s should have aliases", vmID)
}
}
func TestChainAliases(t *testing.T) {
require.NotEmpty(t, PChainAliases)
require.NotEmpty(t, XChainAliases)
require.NotEmpty(t, CChainAliases)
require.Contains(t, PChainAliases, "P")
require.Contains(t, XChainAliases, "X")
require.Contains(t, CChainAliases, "C")
}
func TestDefaultFeeConfigs(t *testing.T) {
// Test dynamic fee configs
configs := []struct {
name string
config interface{}
}{
{"MainnetDynamic", MainnetDynamicFeeConfig},
{"TestnetDynamic", TestnetDynamicFeeConfig},
{"LocalDynamic", LocalDynamicFeeConfig},
{"MainnetValidator", MainnetValidatorFeeConfig},
{"TestnetValidator", TestnetValidatorFeeConfig},
{"LocalValidator", LocalValidatorFeeConfig},
}
for _, tt := range configs {
t.Run(tt.name, func(t *testing.T) {
require.NotNil(t, tt.config)
})
}
}
func TestFromConfigExplicitStakers(t *testing.T) {
require := require.New(t)
// Build 5 stakers with explicit Weight and NodeID (no BLS keys)
nodeIDs := []string{
"NodeID-7D3wajA7bNpfyHpfEtkjUF1KcuhFEfPbZ",
"NodeID-A6d3tQtteyaBCiffij3ohxxgq17DdvChs",
"NodeID-Kv37z2BuRsGPhYNeCyktfgeYHK5QbZMUu",
"NodeID-HvrW5UP1SdR8ujEHzCdUurhvUj1Liydxo",
"NodeID-LBhpNZ7Sf9gpT1gJv2MKKS4Yd2SibTVeK",
}
var secpAddr ids.ShortID
stakerHash := hash.ComputeHash160([]byte("test-staker-v1"))
copy(secpAddr[:], stakerHash)
startTime := uint64(time.Now().Unix())
const hundredYears = 100 * 365 * 24 * 60 * 60
const oneBillionLUX = 1_000_000_000_000_000_000
const oneMillionLUX = 1_000_000_000_000_000
stakers := make([]genesiscfg.Staker, len(nodeIDs))
for i, nidStr := range nodeIDs {
nodeID, err := ids.NodeIDFromString(nidStr)
require.NoError(err)
stakers[i] = genesiscfg.Staker{
NodeID: nodeID,
RewardAddress: secpAddr,
DelegationFee: 1000000,
Weight: oneBillionLUX / uint64(len(nodeIDs)),
StartTime: startTime,
EndTime: startTime + hundredYears,
}
}
var ethShortID ids.ShortID
cfg := &genesiscfg.Config{
NetworkID: constants.LocalID,
Allocations: []genesiscfg.Allocation{
{
EVMAddr: secpAddr,
UTXOAddr: secpAddr,
InitialAmount: 0,
UnlockSchedule: []genesiscfg.LockedAmount{
{Amount: oneBillionLUX, Locktime: 0},
},
},
{
EVMAddr: ethShortID,
UTXOAddr: ethShortID,
InitialAmount: oneMillionLUX,
},
},
StartTime: startTime,
InitialStakeDuration: hundredYears,
InitialStakeDurationOffset: 0,
InitialStakedFunds: []ids.ShortID{secpAddr},
InitialStakers: stakers,
CChainGenesis: `{"config":{"chainId":31337},"alloc":{}}`,
Message: "Test Genesis",
}
genesisBytes, _, err := FromConfig(cfg)
require.NoError(err)
require.NotEmpty(genesisBytes)
// Parse genesis and verify validators
parsed, err := genesis.Parse(genesisBytes)
require.NoError(err)
require.Len(parsed.Validators, 5, "expected 5 validators in genesis")
for i, vdrTx := range parsed.Validators {
switch ut := vdrTx.Unsigned.(type) {
case *txs.AddValidatorTx:
require.Equal(stakers[i].Weight, ut.Weight(),
"validator %d weight mismatch", i)
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
case *txs.AddPermissionlessValidatorTx:
require.Equal(stakers[i].Weight, ut.Weight(),
"validator %d weight mismatch", i)
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
default:
t.Fatalf("unexpected validator tx type: %T", ut)
}
}
}
// TestFromConfigExplicitStakersNoStakedFunds reproduces the bug: when
// InitialStakedFunds is empty but stakers have explicit Weight, the validators
// end up with empty StakeOuts and the P-Chain rejects them.
func TestFromConfigExplicitStakersNoStakedFunds(t *testing.T) {
require := require.New(t)
nodeIDs := []string{
"NodeID-7D3wajA7bNpfyHpfEtkjUF1KcuhFEfPbZ",
"NodeID-A6d3tQtteyaBCiffij3ohxxgq17DdvChs",
"NodeID-Kv37z2BuRsGPhYNeCyktfgeYHK5QbZMUu",
"NodeID-HvrW5UP1SdR8ujEHzCdUurhvUj1Liydxo",
"NodeID-LBhpNZ7Sf9gpT1gJv2MKKS4Yd2SibTVeK",
}
var deployerAddr ids.ShortID
deployerHash := hash.ComputeHash160([]byte("test-deployer"))
copy(deployerAddr[:], deployerHash)
startTime := uint64(time.Now().Unix())
const hundredYears = 100 * 365 * 24 * 60 * 60
const oneBillionLUX = 1_000_000_000_000_000_000
const oneMillionLUX = 1_000_000_000_000_000
stakers := make([]genesiscfg.Staker, len(nodeIDs))
for i, nidStr := range nodeIDs {
nodeID, err := ids.NodeIDFromString(nidStr)
require.NoError(err)
stakers[i] = genesiscfg.Staker{
NodeID: nodeID,
RewardAddress: deployerAddr,
DelegationFee: 1000000,
Weight: oneBillionLUX / uint64(len(nodeIDs)),
StartTime: startTime,
EndTime: startTime + hundredYears,
}
}
// No InitialStakedFunds -- stakers have explicit Weight only
cfg := &genesiscfg.Config{
NetworkID: constants.LocalID,
Allocations: []genesiscfg.Allocation{
{
EVMAddr: deployerAddr,
UTXOAddr: deployerAddr,
InitialAmount: oneMillionLUX,
},
},
StartTime: startTime,
InitialStakeDuration: hundredYears,
InitialStakeDurationOffset: 0,
InitialStakedFunds: nil, // No staked funds
InitialStakers: stakers,
CChainGenesis: `{"config":{"chainId":31337},"alloc":{}}`,
Message: "Test Genesis No Staked Funds",
}
genesisBytes, _, err := FromConfig(cfg)
require.NoError(err)
require.NotEmpty(genesisBytes)
parsed, err := genesis.Parse(genesisBytes)
require.NoError(err)
require.Len(parsed.Validators, 5, "expected 5 validators in genesis")
for i, vdrTx := range parsed.Validators {
switch ut := vdrTx.Unsigned.(type) {
case *txs.AddValidatorTx:
require.Greater(ut.Weight(), uint64(0), "validator %d weight must be non-zero", i)
require.Greater(len(ut.StakeOuts()), 0, "validator %d must have stake outputs", i)
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
case *txs.AddPermissionlessValidatorTx:
require.Greater(ut.Weight(), uint64(0), "validator %d weight must be non-zero", i)
require.Greater(len(ut.StakeOuts()), 0, "validator %d must have stake outputs", i)
t.Logf("Validator %d: NodeID=%s Weight=%d StakeOuts=%d",
i, ut.Validator().NodeID, ut.Weight(), len(ut.StakeOuts()))
default:
t.Fatalf("unexpected validator tx type: %T", ut)
}
}
}
// TestFromConfigLegacyShapedBackCompat proves the back-compat rule for
// legacy legacy-shaped pchain configs where initialAmount AND unlockSchedule
// are both set and initialAmount == sum(unlockSchedule). These are two views
// of the same total. Emitting both would double-mint. The builder must emit
// exactly one UTXO per unlock entry (no additional initialAmount UTXO).
func TestFromConfigLegacyShapedBackCompat(t *testing.T) {
require := require.New(t)
var stakerAddr ids.ShortID
copy(stakerAddr[:], hash.ComputeHash160([]byte("legacy-staker")))
var holderAddr ids.ShortID
copy(holderAddr[:], hash.ComputeHash160([]byte("legacy-holder")))
startTime := uint64(time.Now().Unix())
const hundredYears = 100 * 365 * 24 * 60 * 60
// holder gets 100 LUX, split into 10 yearly unlocks of 10 LUX each.
// initialAmount is the total (legacy convention).
const holderTotal = 100_000_000_000_000 // 100 LUX in nLUX
const perUnlock = holderTotal / 10
holderUnlocks := make([]genesiscfg.LockedAmount, 10)
for i := range holderUnlocks {
holderUnlocks[i] = genesiscfg.LockedAmount{
Amount: perUnlock,
Locktime: startTime + uint64(i)*365*24*60*60,
}
}
nodeID, err := ids.NodeIDFromString("NodeID-7D3wajA7bNpfyHpfEtkjUF1KcuhFEfPbZ")
require.NoError(err)
cfg := &genesiscfg.Config{
NetworkID: constants.LocalID,
Allocations: []genesiscfg.Allocation{
{
EVMAddr: stakerAddr,
UTXOAddr: stakerAddr,
InitialAmount: 0,
UnlockSchedule: []genesiscfg.LockedAmount{
{Amount: holderTotal, Locktime: 0},
},
},
{
// Legacy legacy-shaped: both fields set, initialAmount
// equals sum(unlockSchedule). The builder MUST treat these
// as one allocation (no double-mint).
EVMAddr: holderAddr,
UTXOAddr: holderAddr,
InitialAmount: holderTotal,
UnlockSchedule: holderUnlocks,
},
},
StartTime: startTime,
InitialStakeDuration: hundredYears,
InitialStakeDurationOffset: 0,
InitialStakedFunds: []ids.ShortID{stakerAddr},
InitialStakers: []genesiscfg.Staker{
{
NodeID: nodeID,
RewardAddress: stakerAddr,
DelegationFee: 1000000,
StartTime: startTime,
EndTime: startTime + hundredYears,
},
},
CChainGenesis: `{"config":{"chainId":31337},"alloc":{}}`,
Message: "Legacy Shaped Back-Compat",
}
genesisBytes, _, err := FromConfig(cfg)
require.NoError(err)
require.NotEmpty(genesisBytes)
parsed, err := genesis.Parse(genesisBytes)
require.NoError(err)
// Count UTXOs belonging to holderAddr. With the back-compat fix the
// builder emits exactly len(holderUnlocks) UTXOs (one per unlock entry)
// and no extra immediately-spendable UTXO for initialAmount. Without
// the fix it would emit len(holderUnlocks)+1 UTXOs (double-mint).
//
// Outputs with locktime in the future are wrapped in *stakeable.LockOut
// whose inner Out is the *secp256k1fx.TransferOutput; outputs that are
// immediately spendable are raw *secp256k1fx.TransferOutput.
var holderUTXOs int
var holderSum uint64
for _, u := range parsed.UTXOs {
var out *secp256k1fx.TransferOutput
switch o := u.Out.(type) {
case *secp256k1fx.TransferOutput:
out = o
case *stakeable.LockOut:
inner, ok := o.TransferableOut.(*secp256k1fx.TransferOutput)
if !ok {
continue
}
out = inner
default:
continue
}
if len(out.OutputOwners.Addrs) != 1 || out.OutputOwners.Addrs[0] != holderAddr {
continue
}
holderUTXOs++
holderSum += out.Amt
}
require.Equal(len(holderUnlocks), holderUTXOs,
"holder must have exactly %d UTXOs (one per unlock entry), got %d (double-mint?)",
len(holderUnlocks), holderUTXOs)
require.Equal(uint64(holderTotal), holderSum,
"holder UTXO sum must equal initialAmount (no double-mint)")
}