mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
/v1/health's `bootstrapped` check publishes chains.Nets.Bootstrapping()
verbatim as its message. Nets.chains is keyed by NET id, and the aggregate
appended the map key rather than asking the net which of its chains had not
converged. So every stuck primary-network chain surfaced as one ID,
11111111111111111111111111111111LpoYY — constants.PrimaryNetworkID, i.e.
ids.Empty — a "chain" the chain manager has never heard of. An operator who
went looking for it got "there is no chain with alias/ID", and N stuck chains
collapsed into a single indistinguishable entry that named none of them.
Measured on lux-devnet luxd-0 (v1.36.23), POST health.health:
"bootstrapped":{"message":["11111111111111111111111111111111LpoYY"],
"error":"chains not bootstrapped","contiguousFailures":3213}
The net owns the bootstrapping set, so the net is what can name those chains:
nets.Net grows Bootstrapping() []ids.ID and chains.Nets aggregates those
instead of its own keys. One place holds the set; one place reads it.
The existing TestNetsBootstrapping asserted the defect (require.Contains
bootstrapping, netID) — that assertion is why it survived review. It now
demands the chain ID and refuses the net ID.
This also cuts the first tag containing dada5a31, the GET /v1/health encoder
fix: apihealth.APIReply carries a time.Duration per check, jsonv2 has no
default representation for it, so every GET degraded to
{"healthy":…,"error":"health reply encode failed"} while the status code still
looked right. Reproduced here directly —
json: cannot marshal from Go time.Duration within "/checks/network/duration"
That fix landed on main on 2026-07-25 but no tag ever carried it: v1.36.28
points at 52de3948, seven commits behind. The fleet runs v1.36.2/23/24, all of
which predate it.
Tests (GOWORK=off CGO_ENABLED=0):
chains ok — TestNetsBootstrappingReportsChainsNotNets fails against the
old aggregate with exactly ids.Empty in the list
nets ok — TestNetBootstrappingNamesTheChains
health ok — the three handler tests fail against the jsonv2 encoder
(checks decode empty) and pass against encoding/json
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
161 lines
4.5 KiB
Go
161 lines
4.5 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
|
|
|
|
// Bootstrapping returns the IDs of the chains in this net that have NOT yet
|
|
// finished initial sync. The net owns the bootstrapping set, so it is the
|
|
// net — not its caller — that can name those chains.
|
|
Bootstrapping() []ids.ID
|
|
|
|
// 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) Bootstrapping() []ids.ID {
|
|
s.lock.RLock()
|
|
defer s.lock.RUnlock()
|
|
|
|
return s.bootstrapping.List()
|
|
}
|
|
|
|
// IsChainBootstrapped assumes MONOTONIC per-process bootstrapped state: a chain
|
|
// only ever moves bootstrapping→bootstrapped (Bootstrapped is forward-only and
|
|
// AddChain refuses to re-add a chain already in either set), so this signal — and
|
|
// the Bootstrapping() health check that shares the set — never report stale-true.
|
|
// INVARIANT: if a future path ever moves a live chain BACK to bootstrapping (e.g.
|
|
// SetState(Bootstrapping) on a running chain) it MUST remove the chainID from
|
|
// bootstrapped, or both this signal and the readiness health check will report a
|
|
// re-syncing chain as still bootstrapped.
|
|
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)
|
|
}
|