Files
zeekayandHanzo Dev d678c2d213 transform: publish upstream simplex as luxfi/bft on luxfi/log
Reapplies the fork's whole delta on top of the merge, as one reviewable diff
rather than 43 conflict resolutions. Nothing here changes consensus behaviour.

- module github.com/luxfi/bft, Go 1.26.4; root package simplex -> bft, which
  would otherwise collide with the simplex/ subpackage under this module name.
- go.uber.org/zap -> github.com/luxfi/log, so common.Logger takes ...log.Field.
  zap.Error maps to log.Err: log.Error is luxfi/log's package-level logging
  function, not a field constructor, and the compiler is the only thing that
  catches that. Needs luxfi/log v1.6.1 for the Stringers constructor.
- avalanchego/ -> host/. The package holds NodeID, VMBlock, Bitmask and size
  limits — the host node's types, vendored to avoid importing a whole node — so
  it is named for what it holds. Renaming it luxd/ would have swapped one brand
  for another and said just as little.
- testutil/logger.go reimplemented without zapcore. Upstream layers a
  keyword-filtering Core, a debug-swallowing Encoder and a Tee because that is
  zap's extension model; luxfi/log has no such seam, so every policy is applied
  in one emit. That also fixes two defects the layering caused:
  SilenceExceptKeywords rebuilt the logger and discarded an installed Intercept
  hook, and the two sinks read different env vars (LOG_LEVEL vs log_level) so
  they disagreed about suppressing debug. Hooks run outside the lock, so a hook
  that logs fails the test instead of deadlocking it. Fatal panics rather than
  exiting, because exiting takes the suite down with no attribution.
- simplex/monitor_test.go keeps a local logger: testutil imports simplex, so an
  internal test cannot import testutil back without closing a cycle.
- README keeps the name Simplex throughout — it is the protocol's own name and
  the subject of reference [1]. Only the deployment target changed. The HyperSDK
  sentence still says Avalanche because that sentence is true.
- Drops the fork's Makefile: it duplicated unit-tests.sh and fuzz.sh, and ran the
  suite with -timeout 30s when simplex alone needs over 100s.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 04:21:17 -07:00

327 lines
8.4 KiB
Go

// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package random_network
import (
"math/rand"
"sync"
"testing"
"time"
"github.com/luxfi/bft/common"
"github.com/luxfi/bft/testutil"
"github.com/luxfi/log"
)
type Network struct {
*testutil.BasicInMemoryNetwork
logger *testutil.TestLogger
t *testing.T
lock sync.Mutex
nodes []*Node
numNodes uint64
randomness *rand.Rand
config *FuzzConfig
// tx stats
allIssuedTxs int
}
func NewNetwork(config *FuzzConfig, t *testing.T) *Network {
logger := CreateNetworkLogger(t, config)
logger.Info("Creating new network with random seed", log.Int64("seed", config.RandomSeed))
r := rand.New(rand.NewSource(config.RandomSeed))
numNodes := r.Intn(config.MaxNodes-config.MinNodes+1) + config.MinNodes
nodeIds := make([]common.NodeID, numNodes)
for i := range numNodes {
nodeIds[i] = []byte{byte(i)}
}
nodes := make([]*Node, numNodes)
logger.Info("Initiating network with nodes", log.Int("num nodes", numNodes))
basicNetwork := testutil.NewBasicInMemoryNetwork(t, nodeIds)
for i := range numNodes {
node := NewNode(t, nodeIds[i], basicNetwork, config, randomNodeConfig{})
basicNetwork.AddNode(node.BasicNode)
nodes[i] = node
}
return &Network{
BasicInMemoryNetwork: basicNetwork,
nodes: nodes,
randomness: r,
logger: logger,
t: t,
config: config,
numNodes: uint64(numNodes),
}
}
func (n *Network) StartInstances() {
panic("Call Run() Instead")
}
func (n *Network) Run() {
n.BasicInMemoryNetwork.StartInstances()
defer n.BasicInMemoryNetwork.StopInstances()
targetHeight := uint64(n.config.NumFinalizedBlocks)
for {
n.crashAndRecoverNodes()
txs := n.issueTxs()
maxHeight := n.getMaxHeight()
minHeight := n.getMinHeight()
n.logger.Info("Issued Transactions", log.Int("count", len(txs)), log.Uint64("min height", minHeight), log.Uint64("max height", maxHeight))
n.waitForTxAcceptance(txs)
maxHeight = n.getMaxHeight()
minHeight = n.getMinHeight()
n.logger.Info("All issued transactions accepted", log.Int("count", len(txs)), log.Uint64("min height", minHeight), log.Uint64("max height", maxHeight))
// get the max height and ensure all nodes recover to that height
n.recoverToHeight(n.getMaxHeight())
if minHeight >= targetHeight {
n.logger.Info("Reached target height", log.Uint64("targetHeight", targetHeight), log.Uint64("minHeight", minHeight))
break
}
}
// if we have gotten this far, the test has succeeded so we can clear the log directory
clearLogDirectory(n.config.LogDirectory)
}
func (n *Network) getMinHeightNodeID() common.NodeID {
minHeight := n.nodes[0].storage.NumBlocks()
minHeightNodeID := n.nodes[0].E.ID
for _, node := range n.nodes[1:] {
height := node.storage.NumBlocks()
if height < minHeight {
minHeight = height
minHeightNodeID = node.E.ID
}
}
return minHeightNodeID
}
func (n *Network) recoverToHeight(height uint64) {
for n.getMinHeight() < height {
n.logger.Debug("Advancing network time", log.Uint64("num crashed nodes", n.numCrashedNodes()),
log.Uint64("min height", n.getMinHeight()),
log.Uint64("max height", n.getMaxHeight()),
log.Stringer("Smallest node ID", n.getMinHeightNodeID()),
log.Uint64("target height", height),
)
for i, node := range n.nodes {
isCrashed := node.isCrashed.Load()
if isCrashed {
// randomly decide to recover based on NodeRecoverPercentage
if n.randomness.Float64() < n.config.NodeRecoverProbability {
n.logger.Debug("Recovering node", log.Stringer("nodeID", node.E.ID))
n.startNode(i)
}
}
}
n.lock.Lock()
n.BasicInMemoryNetwork.AdvanceTime(n.config.AdvanceTimeTickAmount)
n.lock.Unlock()
}
}
func (n *Network) issueTxs() []*TX {
n.lock.Lock()
defer n.lock.Unlock()
numTxs := n.randomness.Intn(n.config.MaxTxsPerIssue-n.config.MinTxsPerIssue+1) + n.config.MinTxsPerIssue // randomize between min and max inclusive
txs := make([]*TX, 0, numTxs)
for range numTxs {
tx := CreateNewTX()
txs = append(txs, tx)
n.allIssuedTxs++
}
// first add to all mempools
for _, node := range n.nodes {
node.mempool.AddPendingTXs(txs...)
}
// then notify all mempools that new txs are ready
for _, node := range n.nodes {
node.mempool.NotifyTxsReady()
}
return txs
}
func (n *Network) waitForTxAcceptance(txs []*TX) {
for {
allAccepted := true
for _, node := range n.nodes {
if node.isCrashed.Load() {
continue
}
if accepted := node.areTxsAccepted(txs); !accepted {
node.mempool.lock.Lock()
n.logger.Debug("Not all txs accepted yet by node", log.Stringer("nodeID", node.E.ID), log.Int("unaccepted txs in mempool", len(node.mempool.unacceptedTxs)))
node.mempool.lock.Unlock()
allAccepted = false
}
}
if allAccepted {
return
}
n.lock.Lock()
n.logger.Debug("Advancing network time to wait for tx acceptance", log.Uint64("num crashed nodes", n.numCrashedNodes()))
n.BasicInMemoryNetwork.AdvanceTime(n.config.AdvanceTimeTickAmount)
n.lock.Unlock()
time.Sleep(20 * time.Millisecond)
}
}
func (n *Network) numCrashedNodes() uint64 {
numCrashed := 0
for _, node := range n.nodes {
if node.isCrashed.Load() {
numCrashed++
}
}
return uint64(numCrashed)
}
func (n *Network) crashAndRecoverNodes() {
if n.config.NodeCrashProbability == 0 {
return
}
f := (int(n.numNodes) - 1) / 3
if f == 0 {
n.logger.Info("Not enough nodes for crash testing", log.Uint64("numNodes", n.numNodes))
return
}
crashedNodes := []common.NodeID{}
recoveredNodes := []common.NodeID{}
maxLeftToCrash := f - int(n.numCrashedNodes())
// go through each node, randomly decide to crash based on NodeCrashPercentage
for i, node := range n.nodes {
isCrashed := node.isCrashed.Load()
if isCrashed {
// randomly decide to recover based on NodeRecoverPercentage
if n.randomness.Float64() < n.config.NodeRecoverProbability {
n.startNode(i)
recoveredNodes = append(recoveredNodes, node.E.ID)
maxLeftToCrash++
}
continue
}
// check if we can still crash more nodes
if maxLeftToCrash <= 0 {
continue
}
// randomly decide to crash the node
if n.randomness.Float64() < n.config.NodeCrashProbability {
maxLeftToCrash--
n.crashNode(i)
crashedNodes = append(crashedNodes, node.E.ID)
}
}
if len(recoveredNodes)+len(crashedNodes) > 0 {
n.logger.Info("Recovered and crashed nodes", log.Stringers("crashed", crashedNodes), log.Stringers("recovered", recoveredNodes), log.Uint64("num crashed", n.numCrashedNodes()))
}
}
func (n *Network) getMinHeight() uint64 {
minHeight := n.nodes[0].storage.NumBlocks()
for _, node := range n.nodes[1:] {
height := node.storage.NumBlocks()
if height < minHeight {
minHeight = height
}
}
return minHeight
}
func (n *Network) getMaxHeight() uint64 {
maxHeight := n.nodes[0].storage.NumBlocks()
for _, node := range n.nodes[1:] {
height := node.storage.NumBlocks()
if height > maxHeight {
maxHeight = height
}
}
return maxHeight
}
func (n *Network) SetInfoLog() {
n.lock.Lock()
defer n.lock.Unlock()
for _, node := range n.nodes {
node.logger.SetLevel(log.InfoLevel)
}
}
func (n *Network) PrintStatus() {
n.lock.Lock()
defer n.lock.Unlock()
// prints the number of nodes
n.logger.Info("Network Status", log.Int("num nodes", len(n.nodes)), log.Int64("Seed", n.config.RandomSeed))
// prints the number of txs in each node's mempool
for _, node := range n.nodes {
n.logger.Info("Node Status", log.Stringer("nodeID", node.E.ID), log.Int("Short", int(node.E.ID[0])), log.Uint64("Round", node.E.Metadata().Round), log.Uint64("Height", node.storage.NumBlocks()))
node.PrintMessageTypesSent()
}
// prints total issued txs and failed txs
n.logger.Info("Transaction Stats", log.Int("total issued txs", n.allIssuedTxs))
}
func (n *Network) crashNode(idx int) {
n.logger.Debug("Crashing node", log.Stringer("nodeID", n.nodes[idx].E.ID))
n.nodes[idx].isCrashed.Store(true)
n.nodes[idx].Stop()
}
func (n *Network) startNode(idx int) {
instance := n.nodes[idx]
nodeID := instance.E.ID
mempool := instance.mempool
clonedWal := instance.wal.Clone()
clonedStorage := instance.storage.Clone()
mempool.Clear()
newNode := NewNode(n.t, nodeID, n.BasicInMemoryNetwork, n.config, randomNodeConfig{
mempool: mempool,
wal: clonedWal,
storage: clonedStorage,
logger: instance.logger,
})
n.nodes[idx] = newNode
n.BasicInMemoryNetwork.ReplaceNode(newNode.BasicNode)
newNode.Start()
}