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>
141 lines
3.6 KiB
Go
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)
|
|
}
|