mirror of
https://github.com/luxfi/bft.git
synced 2026-10-30 17:01:54 +00:00
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]>
197 lines
5.1 KiB
Go
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())
|
|
}
|