Files
node/nets/net.go
T
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

141 lines
3.6 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package nets
import (
"sync"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
)
type chain struct {
lock sync.RWMutex
bootstrapping set.Set[ids.ID]
bootstrapped set.Set[ids.ID]
once sync.Once
bootstrappedSema chan struct{}
config Config
myNodeID ids.NodeID
}
var _ Net = (*chain)(nil)
type Allower interface {
// IsAllowed filters out nodes that are not allowed to connect to this chain
IsAllowed(nodeID ids.NodeID, isValidator bool) bool
}
// Net keeps track of the currently bootstrapping chains in a chain. If no
// chains in the net are currently bootstrapping, the net is considered
// bootstrapped.
type Net interface {
// IsBootstrapped returns true if the chains in this chain are done bootstrapping
IsBootstrapped() bool
// IsChainBootstrapped reports whether a SPECIFIC chain in this net has finished
// initial sync — i.e. Bootstrapped(chainID) was called for it (the chain reached
// the network frontier and its VM went to normal operation). This is the per-chain
// truth that info.isBootstrapped keys on, distinct from IsBootstrapped() which is
// the net-wide "no chain still bootstrapping" aggregate. A chain that is merely
// tracked (added, sync goroutine launched) but has NOT converged is still in the
// bootstrapping set and reads false here — closing the premature-true masking bug.
IsChainBootstrapped(chainID ids.ID) bool
// Bootstrapped marks the chain as done bootstrapping
Bootstrapped(chainID ids.ID)
// OnBootstrapCompleted is called when bootstrapping completes
OnBootstrapCompleted() error
// AddChain adds a chain to this Net
AddChain(chainID ids.ID) bool
// Config returns config of this Net
Config() Config
Allower
}
func New(myNodeID ids.NodeID, config Config) Net {
return &chain{
bootstrapping: make(set.Set[ids.ID]),
bootstrapped: make(set.Set[ids.ID]),
bootstrappedSema: make(chan struct{}),
config: config,
myNodeID: myNodeID,
}
}
func (s *chain) IsBootstrapped() bool {
s.lock.RLock()
defer s.lock.RUnlock()
return s.bootstrapping.Len() == 0
}
func (s *chain) IsChainBootstrapped(chainID ids.ID) bool {
s.lock.RLock()
defer s.lock.RUnlock()
return s.bootstrapped.Contains(chainID)
}
func (s *chain) Bootstrapped(chainID ids.ID) {
s.lock.Lock()
defer s.lock.Unlock()
s.bootstrapping.Remove(chainID)
s.bootstrapped.Add(chainID)
if s.bootstrapping.Len() > 0 {
return
}
s.once.Do(func() {
close(s.bootstrappedSema)
})
}
func (s *chain) AllBootstrapped() <-chan struct{} {
return s.bootstrappedSema
}
func (s *chain) OnBootstrapCompleted() error {
// Mark net as having completed bootstrap
// This is called when all chains in the net have bootstrapped
return nil
}
func (s *chain) OnBootstrapStarted() error {
// Mark net as starting bootstrap
return nil
}
func (s *chain) AddChain(chainID ids.ID) bool {
s.lock.Lock()
defer s.lock.Unlock()
if s.bootstrapping.Contains(chainID) || s.bootstrapped.Contains(chainID) {
return false
}
s.bootstrapping.Add(chainID)
return true
}
func (s *chain) Config() Config {
return s.config
}
func (s *chain) IsAllowed(nodeID ids.NodeID, isValidator bool) bool {
// Case 1: NodeID is this node
// Case 2: This net is not validator-only chain
// Case 3: NodeID is a validator for this chain
// Case 4: NodeID is explicitly allowed whether it's net validator or not
return nodeID == s.myNodeID ||
!s.config.ValidatorOnly ||
isValidator ||
s.config.AllowedNodes.Contains(nodeID)
}