Files
node/chains/manager_test.go
zeekayandHanzo Dev a63a18bf71 node v1.32.11: C-Chain restart/recovery serving hardening (isBootstrapped truth + wipe-path ancestry)
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>
2026-07-02 00:05:36 -07:00

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")
}