mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
Two node/bootstrap-layer fixes for a clean rolling validator upgrade. Consensus engine untouched (v1.35.5 boot-seed from v1.32.10 carried forward). [HIGH — correctness] info.isBootstrapped(C) tracks REAL state, not premature true. manager.IsBootstrapped(id) returned true the instant a chain merely EXISTED in m.chains (set right after createChain launched the async, possibly-stalling bootstrap goroutine) — so a C-Chain stalled at genesis (head 0x0) reported info.isBootstrapped(C)=true, masking the stall from any wait-for-healthy gate. Now keys on the SAME sb.Bootstrapped signal the readiness health check uses (m.Nets.IsChainBootstrapped), set only after runInitialSync reaches the named frontier and the VM goes to normal operation (head advanced, eth-RPC live). GetChains().Bootstrapped fixed identically. New per-chain query on nets.Net + chains.Nets (nil-safe). Regression test TestIsBootstrappedTracksRealConvergence. [MEDIUM — robustness] WIPE-path GetAncestors fetch hardened for ≥2 peers at genesis. Applying the proven avalanchego contract (studied in snowman/bootstrap + getter): (a) Ancestors buffers the whole sample and SKIPS empty batches, returning the first NON-EMPTY one — a size-1 channel let a fast empty reply from a genesis peer win the race and starve a peer that actually held the ancestry; (b) sampleAncestorBeacons PREFERS beacons the frontier round found genuinely ahead (they hold the ancestry) — ava's PeerTracker "ask a prover" bias sourced from the replies we already have, no separate tracker. Tests: TestNodeBootstrap_WipePath_MixedGenesisPeers_ObtainsAncestry, TestSampleAncestorBeacons_PrefersAheadBeacons. Build: main+chains+nets+service/info compile CGO_ENABLED=0 (ARC/Dockerfile path); full chains+nets suites green. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
336 lines
9.1 KiB
Go
336 lines
9.1 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package chains
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/metric"
|
|
"github.com/luxfi/node/nets"
|
|
"github.com/luxfi/node/vms"
|
|
"github.com/luxfi/vm"
|
|
)
|
|
|
|
// TestNew tests creating a new manager
|
|
func TestNew(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
config := &ManagerConfig{
|
|
SkipBootstrap: true,
|
|
EnableAutomining: true,
|
|
Log: log.NewNoOpLogger(),
|
|
Metrics: metric.NewMultiGatherer(),
|
|
VMManager: vms.NewManager(),
|
|
ChainDataDir: t.TempDir(),
|
|
}
|
|
|
|
m, err := New(config)
|
|
require.NoError(err)
|
|
require.NotNil(m)
|
|
|
|
// Cast to implementation to check internal state
|
|
mImpl := m.(*manager)
|
|
require.True(mImpl.SkipBootstrap)
|
|
require.True(mImpl.EnableAutomining)
|
|
require.NotNil(mImpl.chains)
|
|
require.NotNil(mImpl.chainsQueue)
|
|
}
|
|
|
|
// TestSkipBootstrapTracker tests that skip bootstrap mode uses correct tracker
|
|
func TestSkipBootstrapTracker(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
// Create a mock tracker for testing
|
|
config := &ManagerConfig{
|
|
SkipBootstrap: true,
|
|
EnableAutomining: true,
|
|
Log: log.NewNoOpLogger(),
|
|
Metrics: metric.NewMultiGatherer(),
|
|
VMManager: vms.NewManager(),
|
|
ChainDataDir: t.TempDir(),
|
|
// Tracker configuration not required for basic manager testing
|
|
}
|
|
|
|
m, err := New(config)
|
|
require.NoError(err)
|
|
require.NotNil(m)
|
|
|
|
// Verify skip bootstrap mode is enabled
|
|
mImpl := m.(*manager)
|
|
require.True(mImpl.SkipBootstrap)
|
|
|
|
// Test that manager can handle bootstrap status queries
|
|
// even when skip bootstrap is enabled
|
|
testChainID := ids.GenerateTestID()
|
|
isBootstrapped := m.IsBootstrapped(testChainID)
|
|
|
|
// When skip bootstrap is enabled, chains should be considered
|
|
// bootstrapped by default, but this specific chain doesn't exist
|
|
// so it returns false
|
|
require.False(isBootstrapped)
|
|
}
|
|
|
|
// TestQueueChainCreation tests queuing chain creation
|
|
func TestQueueChainCreation(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
// Create chains with primary network config
|
|
chainConfigs := map[ids.ID]nets.Config{
|
|
constants.PrimaryNetworkID: {},
|
|
}
|
|
chains, err := NewNets(ids.GenerateTestNodeID(), chainConfigs)
|
|
require.NoError(err)
|
|
|
|
config := &ManagerConfig{
|
|
Log: log.NewNoOpLogger(),
|
|
Metrics: metric.NewMultiGatherer(),
|
|
VMManager: vms.NewManager(),
|
|
ChainDataDir: t.TempDir(),
|
|
Nets: chains,
|
|
}
|
|
|
|
m, err := New(config)
|
|
require.NoError(err)
|
|
|
|
mImpl := m.(*manager)
|
|
|
|
// Create test chain parameters
|
|
chainID := ids.GenerateTestID()
|
|
netID := ids.GenerateTestID()
|
|
chainParams := ChainParameters{
|
|
ID: chainID,
|
|
ChainID: netID,
|
|
VMID: ids.GenerateTestID(),
|
|
}
|
|
|
|
// Queue the chain
|
|
m.QueueChainCreation(chainParams)
|
|
|
|
// Check that the chain was queued
|
|
queuedParams, ok := mImpl.chainsQueue.PopLeft()
|
|
require.True(ok)
|
|
require.Equal(chainParams.ID, queuedParams.ID)
|
|
require.Equal(chainParams.ChainID, queuedParams.ChainID)
|
|
require.Equal(chainParams.VMID, queuedParams.VMID)
|
|
}
|
|
|
|
// TestLookup tests chain alias lookup
|
|
func TestLookup(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
config := &ManagerConfig{
|
|
Log: log.NewNoOpLogger(),
|
|
Metrics: metric.NewMultiGatherer(),
|
|
VMManager: vms.NewManager(),
|
|
ChainDataDir: t.TempDir(),
|
|
}
|
|
|
|
m, err := New(config)
|
|
require.NoError(err)
|
|
|
|
// Create a test chain ID and alias
|
|
chainID := ids.GenerateTestID()
|
|
alias := "test-chain"
|
|
|
|
// Add the alias
|
|
require.NoError(m.Alias(chainID, alias))
|
|
|
|
// Lookup by alias
|
|
lookedUpID, err := m.Lookup(alias)
|
|
require.NoError(err)
|
|
require.Equal(chainID, lookedUpID)
|
|
|
|
// According to the comment in manager.go, the string representation of a chain's ID
|
|
// is also considered to be an alias of the chain. So we need to add it explicitly.
|
|
require.NoError(m.Alias(chainID, chainID.String()))
|
|
|
|
// Now lookup by ID string should work
|
|
lookedUpID, err = m.Lookup(chainID.String())
|
|
require.NoError(err)
|
|
require.Equal(chainID, lookedUpID)
|
|
}
|
|
|
|
// TestIsBootstrapped tests checking if a chain is bootstrapped
|
|
func TestIsBootstrapped(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
config := &ManagerConfig{
|
|
Log: log.NewNoOpLogger(),
|
|
Metrics: metric.NewMultiGatherer(),
|
|
VMManager: vms.NewManager(),
|
|
ChainDataDir: t.TempDir(),
|
|
}
|
|
|
|
m, err := New(config)
|
|
require.NoError(err)
|
|
|
|
// Test non-existent chain
|
|
chainID := ids.GenerateTestID()
|
|
require.False(m.IsBootstrapped(chainID))
|
|
}
|
|
|
|
// TestIsBootstrappedTracksRealConvergence is the regression guard for the
|
|
// premature-true masking bug: manager.IsBootstrapped must report true ONLY once the
|
|
// chain has ACTUALLY finished initial sync (its net marked it Bootstrapped), NOT the
|
|
// instant it is merely tracked (added to m.chains with its sync goroutine launched).
|
|
// Before the fix, a C-Chain stalled at genesis (head 0x0) reported
|
|
// info.isBootstrapped(C)=true, masking the stall from any readiness gate.
|
|
func TestIsBootstrappedTracksRealConvergence(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
chainConfigs := map[ids.ID]nets.Config{
|
|
constants.PrimaryNetworkID: {},
|
|
}
|
|
netsTracker, err := NewNets(ids.GenerateTestNodeID(), chainConfigs)
|
|
require.NoError(err)
|
|
|
|
config := &ManagerConfig{
|
|
Log: log.NewNoOpLogger(),
|
|
Metrics: metric.NewMultiGatherer(),
|
|
VMManager: vms.NewManager(),
|
|
ChainDataDir: t.TempDir(),
|
|
Nets: netsTracker,
|
|
}
|
|
m, err := New(config)
|
|
require.NoError(err)
|
|
mImpl := m.(*manager)
|
|
|
|
// A native chain validated by the primary network — the C-Chain shape.
|
|
chainID := ids.GenerateTestID()
|
|
|
|
// Simulate createChain's tracking: the chain EXISTS in m.chains and is registered
|
|
// as bootstrapping in its validation net — but has NOT converged (initial sync is
|
|
// still driving, e.g. stalled at genesis fetching ancestry).
|
|
mImpl.chainsLock.Lock()
|
|
mImpl.chains[chainID] = &chainInfo{Name: "C-Chain"}
|
|
mImpl.chainsLock.Unlock()
|
|
sb, _ := netsTracker.GetOrCreate(constants.PrimaryNetworkID)
|
|
require.True(sb.AddChain(chainID))
|
|
|
|
// THE FIX: exists-but-not-converged must be FALSE (was true — the masking bug).
|
|
require.False(m.IsBootstrapped(chainID),
|
|
"a tracked-but-still-syncing chain must not report bootstrapped")
|
|
for _, ci := range m.(*manager).GetChains() {
|
|
if ci.ID == chainID {
|
|
require.False(ci.Bootstrapped, "GetChains must not report a syncing chain bootstrapped")
|
|
}
|
|
}
|
|
|
|
// Initial sync reaches the frontier → monitorBootstrap calls sb.Bootstrapped.
|
|
sb.Bootstrapped(chainID)
|
|
|
|
// Now — and only now — it reports bootstrapped (head advanced to frontier, VM live).
|
|
require.True(m.IsBootstrapped(chainID),
|
|
"a converged chain must report bootstrapped")
|
|
found := false
|
|
for _, ci := range m.(*manager).GetChains() {
|
|
if ci.ID == chainID {
|
|
found = true
|
|
require.True(ci.Bootstrapped, "GetChains must report a converged chain bootstrapped")
|
|
}
|
|
}
|
|
require.True(found)
|
|
}
|
|
|
|
// TestToEngineChannelFlow verifies the toEngine channel notification flow
|
|
// This tests the goroutine that reads from toEngine and triggers block building
|
|
func TestToEngineChannelFlow(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
// Create toEngine channel (same as what manager creates)
|
|
toEngine := make(chan vm.Message, 1)
|
|
defer close(toEngine)
|
|
|
|
// Track block builds
|
|
var buildCalls int
|
|
var mu sync.Mutex
|
|
|
|
// Simulate the goroutine that reads from toEngine
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
for msg := range toEngine {
|
|
if msg.Type == 0 { // PendingTxs
|
|
mu.Lock()
|
|
buildCalls++
|
|
mu.Unlock()
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Send PendingTxs notification
|
|
toEngine <- vm.Message{Type: 0} // PendingTxs = 0
|
|
|
|
// Give goroutine time to process
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
mu.Lock()
|
|
count := buildCalls
|
|
mu.Unlock()
|
|
|
|
require.Equal(1, count, "Expected 1 build call after PendingTxs notification")
|
|
|
|
// Send multiple notifications
|
|
for i := 0; i < 5; i++ {
|
|
toEngine <- vm.Message{Type: 0}
|
|
}
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
mu.Lock()
|
|
count = buildCalls
|
|
mu.Unlock()
|
|
|
|
require.Equal(6, count, "Expected 6 total build calls")
|
|
}
|
|
|
|
// TestToEngineMessageTypes verifies different message types are handled correctly
|
|
func TestToEngineMessageTypes(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
toEngine := make(chan vm.Message, 10)
|
|
defer close(toEngine)
|
|
|
|
var pendingTxsCalls int
|
|
var otherCalls int
|
|
var mu sync.Mutex
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
for msg := range toEngine {
|
|
mu.Lock()
|
|
if msg.Type == 0 { // PendingTxs
|
|
pendingTxsCalls++
|
|
} else {
|
|
otherCalls++
|
|
}
|
|
mu.Unlock()
|
|
}
|
|
}()
|
|
|
|
// Send different message types
|
|
toEngine <- vm.Message{Type: 0} // PendingTxs - should trigger build
|
|
toEngine <- vm.Message{Type: 1} // StateSyncDone - should NOT trigger build
|
|
toEngine <- vm.Message{Type: 0} // PendingTxs - should trigger build
|
|
toEngine <- vm.Message{Type: 2} // Unknown - should NOT trigger build
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
mu.Lock()
|
|
pendingCount := pendingTxsCalls
|
|
otherCount := otherCalls
|
|
mu.Unlock()
|
|
|
|
require.Equal(2, pendingCount, "Expected 2 PendingTxs messages")
|
|
require.Equal(2, otherCount, "Expected 2 other messages")
|
|
}
|