Files
zeekay 7cf87b230b node-unified is the node
The module already called itself github.com/luxfi/node while containing no node
in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and
the Go column of the matrix measured a VM plugin. A repo that fetches the thing
it claims to be has not replaced it, so the Go sources move in — 1515 files,
the daemon and everything under it — and the build reaches outside this module
for nothing.

Rust and C++ sit beside it unchanged and are still measured against it by the
differential. That is the point of one repo: the reference implementation and
the two ports are the same checkout, so parity is a thing you can run rather
than a thing you assert.

Verified: builds, and the closure carries no lux-private — checked at build time
rather than believed. The daemon starts twelve chains on a local network
(P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
2026-09-07 16:15:25 -07:00

300 lines
9.8 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package platformvm
import (
"errors"
"sync"
"time"
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/validators/uptime"
)
var (
errAlreadyStartedTracking = errors.New("uptime tracker already started tracking")
errNotStartedTracking = errors.New("uptime tracker has not started tracking")
)
// uptimeTracker implements uptime.Calculator plus the connection/tracking hooks
// the platform VM drives directly (Connect, Disconnect, StartTracking,
// StopTracking, IsConnected).
//
// It is written against the luxfi validators uptime.State interface, which keys
// by netID and returns lastUpdated encoded as a second-granular duration since
// the Unix epoch.
//
// Model, in one sentence: each connected peer's session accrues into a per
// validator up-duration that is folded forward to "now" on read, and persisted
// on flush.
//
// - Connect records a peer's connection time. It is captured from the very
// first handshake — including during bootstrap, before StartTracking — so a
// stable, continuously-connected validator set is fully observed the moment
// tracking begins.
// - StartTracking, called once at P-chain normal-operations start, baselines
// every validator, crediting the pre-tracking window as online, and
// switches into live-tracking mode.
// - CalculateUptime folds the currently-connected session forward to now, so a
// validator that stays connected accrues up-duration WITHOUT ever needing a
// Disconnect.
// - Disconnect / StopTracking flush the accrued session into persistent state.
//
// The prior custom tracker could not accrue uptime for a stable set: it never
// started tracking, only flushed on Disconnect, and — because peer connection
// events were never delivered to the VM — never populated its connected map.
type uptimeTracker struct {
mu sync.RWMutex
clk func() time.Time
state uptime.State
netID ids.ID
// connections maps a currently-connected nodeID to the (second-granular)
// time it connected.
connections map[ids.NodeID]time.Time
// startedTracking gates live-session accounting. Before StartTracking, a
// validator is assumed online since its last persisted update (so the
// bootstrap window is not spuriously counted as downtime). After
// StartTracking, only genuinely-connected sessions accrue.
startedTracking bool
}
func newUptimeTracker(state uptime.State, netID ids.ID, clk func() time.Time) *uptimeTracker {
return &uptimeTracker{
clk: clk,
state: state,
netID: netID,
connections: make(map[ids.NodeID]time.Time),
}
}
// now returns the tracker clock truncated to second precision. lastUpdated is
// persisted at second granularity (state stores lastUpdated.Unix()), so working
// at one resolution keeps the interval arithmetic exact and side-steps
// monotonic-clock skew.
func (t *uptimeTracker) now() time.Time {
return time.Unix(t.clk().Unix(), 0)
}
// lastUpdatedFromDuration reconstructs the persisted lastUpdated timestamp from
// the second-granular "duration since epoch" the uptime.State returns.
func lastUpdatedFromDuration(d time.Duration) time.Time {
return time.Unix(int64(d/time.Second), 0)
}
// Connect records that [nodeID] connected. It is idempotent: a duplicate Connect
// for an already-connected node keeps the original connection time, so repeated
// router dispatch of the same live connection can never reset the session clock.
func (t *uptimeTracker) Connect(nodeID ids.NodeID) {
t.mu.Lock()
defer t.mu.Unlock()
if _, ok := t.connections[nodeID]; ok {
return
}
t.connections[nodeID] = t.now()
}
// IsConnected reports whether [nodeID] currently has a live connection.
func (t *uptimeTracker) IsConnected(nodeID ids.NodeID) bool {
t.mu.RLock()
defer t.mu.RUnlock()
_, connected := t.connections[nodeID]
return connected
}
// Disconnect records that [nodeID] disconnected, flushing its accrued session
// into persistent state. It returns whether that flush actually wrote uptime
// state (a SetUptime the caller must Commit). Flushing is a no-op — mutated
// false — before StartTracking (the session is not yet being measured) and for
// a peer with no uptime record (a non-validator), so the
// caller can skip an empty state.Commit.
func (t *uptimeTracker) Disconnect(nodeID ids.NodeID) (mutated bool, err error) {
t.mu.Lock()
defer t.mu.Unlock()
defer delete(t.connections, nodeID)
if !t.startedTracking {
return false, nil
}
return t.updateUptimeLocked(nodeID)
}
// StartTracking baselines every validator in [nodeIDs] and switches into live
// tracking mode. It is called once at P-chain normal-operations start. Each
// validator's persisted up-duration is advanced to now assuming it was online
// since its last update, so a validator
// that has been in the set is not penalized for the un-measured pre-tracking
// window.
func (t *uptimeTracker) StartTracking(nodeIDs []ids.NodeID) error {
t.mu.Lock()
defer t.mu.Unlock()
if t.startedTracking {
return errAlreadyStartedTracking
}
for _, nodeID := range nodeIDs {
if _, err := t.updateUptimeLocked(nodeID); err != nil {
return err
}
}
t.startedTracking = true
return nil
}
// StopTracking flushes every validator in [nodeIDs] and leaves tracking mode.
// It is called on the normal-ops → bootstrapping / shutdown transition so the
// connected sessions are durably persisted before the node stops measuring.
func (t *uptimeTracker) StopTracking(nodeIDs []ids.NodeID) error {
t.mu.Lock()
defer t.mu.Unlock()
if !t.startedTracking {
return errNotStartedTracking
}
for _, nodeID := range nodeIDs {
if _, err := t.updateUptimeLocked(nodeID); err != nil {
return err
}
}
t.startedTracking = false
return nil
}
// StartedTracking reports whether StartTracking has run and StopTracking has not
// since. The VM lifecycle uses it to avoid double-start and to gate shutdown
// flushing.
func (t *uptimeTracker) StartedTracking() bool {
t.mu.RLock()
defer t.mu.RUnlock()
return t.startedTracking
}
// calculateUptimeLocked returns [nodeID]'s up-duration folded forward to now,
// plus that now (the
// new lastUpdated). The caller must hold t.mu (read or write).
func (t *uptimeTracker) calculateUptimeLocked(nodeID ids.NodeID) (time.Duration, time.Time, error) {
upDuration, lastUpdatedDur, err := t.state.GetUptime(nodeID, t.netID)
if err != nil {
return 0, time.Time{}, err
}
lastUpdated := lastUpdatedFromDuration(lastUpdatedDur)
now := t.now()
// Clock skew: never subtract time or double-count.
if now.Before(lastUpdated) {
return upDuration, lastUpdated, nil
}
// Before tracking, assume the node was online since its last update.
if !t.startedTracking {
return upDuration + now.Sub(lastUpdated), now, nil
}
// Tracking, but not connected: offline since its last update.
connectedAt, isConnected := t.connections[nodeID]
if !isConnected {
return upDuration, now, nil
}
// Tracking and connected: credit from the later of (connect, lastUpdated) so
// no interval is double-counted.
if connectedAt.Before(lastUpdated) {
connectedAt = lastUpdated
}
if now.Before(connectedAt) {
return upDuration, now, nil
}
return upDuration + now.Sub(connectedAt), now, nil
}
// updateUptimeLocked persists the current up-duration for [nodeID], returning
// whether it actually wrote (mutated). A node without a state record (i.e. not a
// current validator) is silently skipped — mutated false — so tracking
// non-validator peers is harmless and never dirties the state. The caller must
// hold t.mu (write).
func (t *uptimeTracker) updateUptimeLocked(nodeID ids.NodeID) (mutated bool, err error) {
upDuration, lastUpdated, err := t.calculateUptimeLocked(nodeID)
if errors.Is(err, database.ErrNotFound) {
return false, nil
}
if err != nil {
return false, err
}
if err := t.state.SetUptime(nodeID, t.netID, upDuration, lastUpdated); err != nil {
return false, err
}
return true, nil
}
// CalculateUptime returns (upDuration, totalDuration) for [nodeID], where
// totalDuration is the maximum possible uptime since the validator's start.
func (t *uptimeTracker) CalculateUptime(nodeID ids.NodeID, netID ids.ID) (time.Duration, time.Duration, error) {
if netID != t.netID {
return 0, 0, nil
}
startTime, err := t.state.GetStartTime(nodeID, netID)
if err != nil {
return 0, 0, err
}
t.mu.RLock()
upDuration, _, err := t.calculateUptimeLocked(nodeID)
t.mu.RUnlock()
if err != nil {
return 0, 0, err
}
total := t.now().Sub(startTime)
if total < 0 {
total = 0
}
return upDuration, total, nil
}
// CalculateUptimePercent returns [nodeID]'s uptime over its whole staking period
// as a fraction in [0, 1].
func (t *uptimeTracker) CalculateUptimePercent(nodeID ids.NodeID, netID ids.ID) (float64, error) {
if netID != t.netID {
return 0, nil
}
startTime, err := t.state.GetStartTime(nodeID, netID)
if err != nil {
return 0, err
}
return t.CalculateUptimePercentFrom(nodeID, netID, startTime)
}
// CalculateUptimePercentFrom returns [nodeID]'s uptime since [from] as a fraction
// in [0, 1].
func (t *uptimeTracker) CalculateUptimePercentFrom(nodeID ids.NodeID, netID ids.ID, from time.Time) (float64, error) {
if netID != t.netID {
return 0, nil
}
t.mu.RLock()
upDuration, _, err := t.calculateUptimeLocked(nodeID)
t.mu.RUnlock()
if err != nil {
return 0, err
}
bestPossible := t.now().Sub(from)
if bestPossible <= 0 {
return 1, nil
}
fraction := float64(upDuration) / float64(bestPossible)
if fraction > 1 {
fraction = 1
}
return fraction, nil
}
// SetCalculator is a no-op: this tracker is a concrete Calculator and does not
// delegate. It exists only to satisfy uptime.Calculator; registration is done by
// the enclosing uptime.LockedCalculator.
func (t *uptimeTracker) SetCalculator(ids.ID, uptime.Calculator) error {
return nil
}