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

197 lines
5.1 KiB
Go

// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package testutil
import (
"bytes"
"sync"
"github.com/luxfi/bft/common"
"github.com/stretchr/testify/require"
)
// MessageFilter is a function type that determines whether a message can be
// transmitted from one node to another.
// Parameters:
// - msg: The message being evaluated for transmission
// - from: The ID of the sending node
// - to: The ID of the receiving node
//
// Returns:
// - bool: true if the message can be transmitted, false otherwise
type MessageFilter func(msg *common.Message, from common.NodeID, to common.NodeID) bool
type NoopComm common.Nodes
func (n NoopComm) Validators() common.Nodes {
return common.Nodes(n)
}
func (n NoopComm) Send(*common.Message, common.NodeID) {
}
func (n NoopComm) Broadcast(msg *common.Message) {
}
type TestComm struct {
from common.NodeID
net *BasicInMemoryNetwork
messageFilter MessageFilter
lock sync.RWMutex
}
func NewTestComm(from common.NodeID, net *BasicInMemoryNetwork, messageFilter MessageFilter) *TestComm {
return &TestComm{
from: from,
net: net,
messageFilter: messageFilter,
}
}
func (c *TestComm) Validators() common.Nodes {
return c.net.nodeWeights
}
func (c *TestComm) Send(msg *common.Message, destination common.NodeID) {
if !c.isMessagePermitted(msg, destination) {
return
}
// cannot send if either [from] or [destination] is not connected
if c.net.IsDisconnected(destination) || c.net.IsDisconnected(c.from) {
for _, instance := range c.net.GetInstances() {
if bytes.Equal(instance.E.ID, destination) {
instance.l.Info("Node is disconnected not sending message")
}
}
return
}
c.maybeTranslateOutgoingToIncomingMessageTypes(msg)
for _, instance := range c.net.GetInstances() {
if bytes.Equal(instance.E.ID, c.from) {
instance.l.Info("Enqueuing message")
continue
}
if bytes.Equal(instance.E.ID, destination) {
instance.enqueue(msg, c.from, destination)
return
}
}
}
func (c *TestComm) SetFilter(filter MessageFilter) {
c.lock.Lock()
defer c.lock.Unlock()
c.messageFilter = filter
}
func (c *TestComm) maybeTranslateOutgoingToIncomingMessageTypes(msg *common.Message) {
if msg.VerifiedReplicationResponse != nil {
data := make([]common.QuorumRound, 0, len(msg.VerifiedReplicationResponse.Data))
for _, verifiedQuorumRound := range msg.VerifiedReplicationResponse.Data {
// Outgoing block is of type verified block but incoming block is of type Block,
// so we do a type cast because the test block implements both.
quorumRound := common.QuorumRound{}
if verifiedQuorumRound.EmptyNotarization != nil {
quorumRound.EmptyNotarization = verifiedQuorumRound.EmptyNotarization
}
if verifiedQuorumRound.VerifiedBlock != nil {
quorumRound.Block = verifiedQuorumRound.VerifiedBlock.(common.Block)
}
if verifiedQuorumRound.Notarization != nil {
quorumRound.Notarization = verifiedQuorumRound.Notarization
}
if verifiedQuorumRound.Finalization != nil {
quorumRound.Finalization = verifiedQuorumRound.Finalization
}
data = append(data, quorumRound)
}
latestRound := verifiedQRtoQR(msg.VerifiedReplicationResponse.LatestRound)
latestSeq := verifiedQRtoQR(msg.VerifiedReplicationResponse.LatestFinalizedSeq)
require.Nil(
c.net.t,
msg.ReplicationResponse,
"message cannot include ReplicationResponse & VerifiedReplicationResponse",
)
msg.ReplicationResponse = &common.ReplicationResponse{
Data: data,
LatestRound: latestRound,
LatestSeq: latestSeq,
}
}
if msg.VerifiedBlockMessage != nil {
require.Nil(c.net.t, msg.BlockMessage, "message cannot include BlockMessage & VerifiedBlockMessage")
msg.BlockMessage = &common.BlockMessage{
Block: msg.VerifiedBlockMessage.VerifiedBlock.(common.Block),
Vote: msg.VerifiedBlockMessage.Vote,
}
}
}
func verifiedQRtoQR(vqr *common.VerifiedQuorumRound) *common.QuorumRound {
if vqr == nil {
return nil
}
qr := &common.QuorumRound{
Notarization: vqr.Notarization,
Finalization: vqr.Finalization,
EmptyNotarization: vqr.EmptyNotarization,
}
if vqr.VerifiedBlock != nil {
qr.Block = vqr.VerifiedBlock.(common.Block)
}
return qr
}
func (c *TestComm) isMessagePermitted(msg *common.Message, destination common.NodeID) bool {
c.lock.RLock()
defer c.lock.RUnlock()
return c.messageFilter(msg, c.from, destination)
}
func (c *TestComm) Broadcast(msg *common.Message) {
if c.net.IsDisconnected(c.from) {
return
}
c.maybeTranslateOutgoingToIncomingMessageTypes(msg)
for _, instance := range c.net.GetInstances() {
if !c.isMessagePermitted(msg, instance.E.ID) {
continue
}
// Skip sending the message to yourself or disconnected nodeWeights
if bytes.Equal(c.from, instance.E.ID) || c.net.IsDisconnected(instance.E.ID) {
continue
}
instance.enqueue(msg, c.from, instance.E.ID)
}
}
// AllowAllMessages allows every message to be sent
func AllowAllMessages(*common.Message, common.NodeID, common.NodeID) bool {
return true
}
func NewNoopComm(nodes common.NodeIDs) NoopComm {
return NoopComm(nodes.EqualWeightedNodes())
}