mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
The uptime event-delivery plumbing (node/chain_router.go + chains/manager.go blockHandler) dropped two invariants avalanchego's handler upholds — the consensus lock and the peer version — each a mainnet-fleet crash. The uptime tracker itself (vms/platformvm/uptime_tracker.go) is RED-cleared and UNCHANGED. CRITICAL #2 — P-chain state race (concurrent map writes -> fatal): chainRouter dispatched Connected/Disconnected on the peer-lifecycle goroutine, where VM.Disconnected -> tracker.Disconnect (state.SetUptime) + state.Commit (state.write) ran concurrently with the block acceptor's state.CommitBatch (state.write) on the engine accept goroutine — no shared lock, so ordinary peer churn triggered Go "concurrent map writes" and crashed the P-chain node. Fix: one VM-owned stateLock serializes every commit of shared platform state that originates outside the engine's lock-free VM.Accept call-out — - block DECISION: block/executor Block.Accept/Reject hold &vm.stateLock (supplied to executor.NewManager) around the whole acceptor visit; - peer/lifecycle: VM.Disconnected and the onReady/onBootstrapStarted/Shutdown Start/StopTracking uptime flushes hold vm.stateLock. This is avalanchego's ctx.Lock invariant (accept serialized with engine.Connected/Disconnected), scoped to the state the platform VM owns and implemented at the VM: the Lux engine invokes VM.Accept as a lock-free call-out (no ctx.Lock over accept exists) and a chain-agnostic blockHandler cannot reach a per-VM accept lock, so routing "through the engine" is not feasible. CRITICAL #1 — C-Chain nil-version panic on state sync: blockHandler.Connected passed connector.Connected(ctx, nodeID, nil). proposervm promotes Connected to coreth, whose state-sync peer tracker compares peer versions; a nil version deref panics any C-Chain node running state sync (a fresh join OR a validator rejoining after falling behind — the launch's core invariant). Fix: plumb the REAL peer version through a versionedConnector capability — chainRouter.Connected converts the node peer version (luxfi/node/version) to the VM boundary type (luxfi/version = chain.VersionInfo) and delivers it via blockHandler.ConnectedWithVersion -> connector.Connected. Audit: geth in-process Connected is a no-op stub (nil-safe); xvm stores the pointer without deref (nil-safe); the real coreth plugin derefs -> fixed by feeding the real version. Tests (SDKROOT + CGO_ENABLED=1, -race): - vms/platformvm/uptime_state_race_test.go: state.Commit (VM.Disconnected path) concurrent with state.CommitBatch (acceptor path) under the shared lock — no race/fatal; verified meaningful (an unlocked probe trips DATA RACE). - chains/blockhandler_connected_version_test.go + node/chain_router_connected_version_test.go: the real, converted version reaches the connector non-nil (dedup covered). Uptime tracker 13/13 and the block/executor reward gate untouched and green; go build ./... and go vet clean. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
151 lines
4.8 KiB
Go
151 lines
4.8 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package platformvm
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/database/memdb"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/metric"
|
|
"github.com/luxfi/runtime"
|
|
validators "github.com/luxfi/validators"
|
|
|
|
"github.com/luxfi/node/upgrade"
|
|
"github.com/luxfi/node/vms/platformvm/config"
|
|
"github.com/luxfi/node/vms/platformvm/genesis/genesistest"
|
|
platformvmmetrics "github.com/luxfi/node/vms/platformvm/metrics"
|
|
"github.com/luxfi/node/vms/platformvm/reward"
|
|
"github.com/luxfi/node/vms/platformvm/state"
|
|
)
|
|
|
|
// newRaceTestState builds a REAL platform state (state.New, memdb + genesis) —
|
|
// the same constructor the VM uses. The genesis carries a validator set, so
|
|
// state.write (invoked by BOTH state.Commit and state.CommitBatch) iterates
|
|
// non-empty validator/metadata maps: exactly the shared maps that mutate
|
|
// concurrently in the fatal this test guards against.
|
|
func newRaceTestState(t *testing.T) state.State {
|
|
t.Helper()
|
|
require := require.New(t)
|
|
|
|
reg := metric.NewRegistry()
|
|
m, err := platformvmmetrics.New(reg)
|
|
require.NoError(err)
|
|
|
|
execCfg, err := config.GetConfig(nil)
|
|
require.NoError(err)
|
|
|
|
st, err := state.New(
|
|
memdb.New(),
|
|
genesistest.NewBytes(t, genesistest.Config{}),
|
|
reg,
|
|
validators.NewManager(),
|
|
upgrade.GetConfig(constants.UnitTestID),
|
|
execCfg,
|
|
&runtime.Runtime{
|
|
NetworkID: constants.UnitTestID,
|
|
ChainID: constants.PlatformChainID,
|
|
Log: log.Noop(),
|
|
},
|
|
m,
|
|
reward.NewCalculator(reward.Config{
|
|
MaxConsumptionRate: 120_000,
|
|
MinConsumptionRate: 100_000,
|
|
MintingPeriod: 365 * 24 * time.Hour,
|
|
SupplyCap: 720 * constants.MegaLux,
|
|
}),
|
|
)
|
|
require.NoError(err)
|
|
return st
|
|
}
|
|
|
|
// TestStateCommitSerializedWithAcceptNoRace is the regression guard for RED
|
|
// CRITICAL #2 (P-chain state race).
|
|
//
|
|
// Before the fix the event-delivery plumbing drove VM.Disconnected on the
|
|
// node's peer-lifecycle goroutine, where it called state.Commit() (→ state.write)
|
|
// concurrently with the block acceptor's state.CommitBatch() (→ state.write) on
|
|
// the consensus accept goroutine. Both mutate the shared currentValidator /
|
|
// staker / metadata maps with no common lock → Go "concurrent map writes" FATAL
|
|
// on ordinary peer churn.
|
|
//
|
|
// The fix installs ONE lock — the platform VM's stateLock — held by BOTH sides:
|
|
// - block DECISION: block/executor Block.Accept/Reject hold &vm.stateLock
|
|
// (supplied to executor.NewManager), around the whole acceptor visit, and
|
|
// - peer/lifecycle commits: VM.Disconnected and the Start/StopTracking uptime
|
|
// flushes hold vm.stateLock directly.
|
|
//
|
|
// This test drives the two REAL racing state operations — state.Commit()
|
|
// (VM.Disconnected's exact call) and state.SetUptime()+state.CommitBatch()
|
|
// (the tracker-flush + acceptor's exact calls) — from two goroutines, each
|
|
// holding the SAME lock the fix installs. Under `-race` it must complete with
|
|
// no data race and no fatal. Remove the shared lock and `-race` immediately
|
|
// reports the concurrent write inside state.write() (the regression).
|
|
func TestStateCommitSerializedWithAcceptNoRace(t *testing.T) {
|
|
st := newRaceTestState(t)
|
|
|
|
// The single serializer the fix installs. In production this is
|
|
// &vm.stateLock, held by Block.Accept (via the executor manager) and by
|
|
// VM.Disconnected/Start/StopTracking.
|
|
var stateLock sync.Mutex
|
|
|
|
// A genesis validator so SetUptime writes a real record (mirrors
|
|
// tracker.Disconnect → state.SetUptime before state.Commit).
|
|
nodeID := genesistest.DefaultNodeIDs[0]
|
|
netID := constants.PrimaryNetworkID
|
|
|
|
const iterations = 300
|
|
var (
|
|
wg sync.WaitGroup
|
|
acceptErr error
|
|
disconnectErr error
|
|
)
|
|
wg.Add(2)
|
|
|
|
// Accept side: the acceptor's state.CommitBatch() + state.Abort() — the exact
|
|
// calls block/executor acceptor.standardBlock makes, both routed through
|
|
// state.write(). Block.Accept holds stateLock around this.
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < iterations; i++ {
|
|
stateLock.Lock()
|
|
_, err := st.CommitBatch()
|
|
st.Abort()
|
|
stateLock.Unlock()
|
|
if err != nil {
|
|
acceptErr = err
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Disconnect side: tracker.Disconnect (state.SetUptime, error ignored exactly
|
|
// as the tracker's updateUptimeLocked ignores ErrNotFound) followed by
|
|
// state.Commit() — VM.Disconnected's exact sequence. Holds the SAME stateLock.
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < iterations; i++ {
|
|
stateLock.Lock()
|
|
_ = st.SetUptime(nodeID, netID, time.Duration(i)*time.Second, time.Unix(int64(i), 0))
|
|
err := st.Commit()
|
|
stateLock.Unlock()
|
|
if err != nil {
|
|
disconnectErr = err
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
require := require.New(t)
|
|
require.NoError(acceptErr)
|
|
require.NoError(disconnectErr)
|
|
}
|