Skip to content

Comm Component for Simplex #3998

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 34 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
dde076d
add simplex pkg and bls structs
samliok Jun 5, 2025
c5be0a4
add simplex to go mod + lint
samliok Jun 5, 2025
5de26b1
added comm, writing tests
samliok Jun 5, 2025
52d343e
add log to createVerifier
samliok Jun 5, 2025
3f74024
add comm tests
samliok Jun 5, 2025
d0f0102
Merge branch 'master' into simplex-bls
samliok Jun 9, 2025
458d540
separate into utils file
samliok Jun 9, 2025
49aa0bb
style improvements
samliok Jun 10, 2025
4dfdb34
use GetValidatorSet for consistent validator set
samliok Jun 11, 2025
475a5ef
Merge branch 'master' into simplex-bls
samliok Jun 11, 2025
8c78c03
pass in membership set to config
samliok Jun 11, 2025
14ad42f
remove comment
samliok Jun 11, 2025
2580d63
Merge branch 'master' into simplex-bls
samliok Jun 12, 2025
ed4422b
test_util simplified
samliok Jun 12, 2025
14aef3e
add replication outbound msgs
samliok Jun 12, 2025
7d3484d
Merge branch 'simplex-bls' into simplex-comm
samliok Jun 12, 2025
b13bc72
lint
samliok Jun 12, 2025
88b06d5
go get
samliok Jun 12, 2025
5f84156
helper function + use testing in utils
samliok Jun 12, 2025
3c362ea
Merge branch 'simplex-bls' into simplex-comm
samliok Jun 12, 2025
03bdc02
Merge branch 'master' into simplex-comm
samliok Jun 18, 2025
ebdbfc9
merge conflicts and cleanup
samliok Jun 18, 2025
7a7e5e7
lint
samliok Jun 18, 2025
d9b8a2c
logs, and err clarification
samliok Jun 18, 2025
c964299
Merge branch 'master' into simplex-comm
samliok Jun 20, 2025
12599d2
add logging to config
samliok Jun 23, 2025
a3820e4
Merge branch 'master' into simplex-comm
samliok Jun 24, 2025
5119e93
Merge branch 'master' into simplex-comm
samliok Jun 30, 2025
84106b0
Merge branch 'master' into simplex-comm
samliok Jul 10, 2025
35c0161
rebase
samliok Jul 10, 2025
e8a91ec
rebase
samliok Jul 10, 2025
ad8ea62
sort nodes in place
samliok Jul 14, 2025
97c816b
update outbound message interface
samliok Jul 14, 2025
91ec652
stray away from mocks + lint
samliok Jul 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions message/messagemock/outbound_message_builder.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions message/outbound_msg_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ type OutboundMsgBuilder interface {
chainID ids.ID,
msg []byte,
) (OutboundMessage, error)

SimplexMessage(
msg *p2p.Simplex,
) (OutboundMessage, error)
}

type outMsgBuilder struct {
Copy link
Contributor

@yacovm yacovm Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this PR, but I have to say I am not sure I understand why this is an un-exported struct. it Makes it impossible to use anywhere in the code (for tests) and we also have only one concrete implementation of it.

@StephenButtolph do you know why?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can utilize this in tests by using message.NewCreator.

Expand Down Expand Up @@ -725,3 +729,15 @@ func (b *outMsgBuilder) AppGossip(chainID ids.ID, msg []byte) (OutboundMessage,
false,
)
}

func (b *outMsgBuilder) SimplexMessage(msg *p2p.Simplex) (OutboundMessage, error) {
return b.builder.createOutbound(
&p2p.Message{
Message: &p2p.Message_Simplex{
Simplex: msg,
},
},
b.compressionType,
false,
)
}
5 changes: 2 additions & 3 deletions simplex/bls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import (
)

func TestBLSVerifier(t *testing.T) {
config, err := newEngineConfig()
require.NoError(t, err)
config := newEngineConfig(t, 1)
signer, verifier := NewBLSAuth(config)
otherNodeID := ids.GenerateTestNodeID()

Expand Down Expand Up @@ -81,7 +80,7 @@ func TestBLSVerifier(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err = verifier.Verify(msg, tt.sig, tt.nodeID)
err := verifier.Verify(msg, tt.sig, tt.nodeID)
require.ErrorIs(t, err, tt.expectErr)
})
}
Expand Down
143 changes: 143 additions & 0 deletions simplex/comm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package simplex

import (
"bytes"
"errors"
"fmt"
"slices"

"github.com/ava-labs/simplex"
"go.uber.org/zap"

"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/message"
"github.com/ava-labs/avalanchego/proto/pb/p2p"
"github.com/ava-labs/avalanchego/snow/engine/common"
"github.com/ava-labs/avalanchego/snow/networking/sender"
"github.com/ava-labs/avalanchego/subnets"
"github.com/ava-labs/avalanchego/utils/set"
)

var (
_ simplex.Communication = (*Comm)(nil)
errNodeNotFound = errors.New("node not found in the validator list")
)

type Comm struct {
logger simplex.Logger
subnetID ids.ID
chainID ids.ID
// nodeID is this nodes ID
nodeID simplex.NodeID
// nodes are the IDs of all the nodes in the subnet
nodes []simplex.NodeID
// sender is used to send messages to other nodes
sender sender.ExternalSender
msgBuilder message.OutboundMsgBuilder
}

func NewComm(config *Config) (*Comm, error) {
nodes := make([]simplex.NodeID, 0, len(config.Validators))

// grab all the nodes that are validators for the subnet
for _, vd := range config.Validators {
nodes = append(nodes, vd.NodeID[:])
}

if _, ok := config.Validators[config.Ctx.NodeID]; !ok {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the plan for non-validator nodes? Is this going to be modified in the future? Or is there going to be some other implementation entirely for non-validators?

Copy link
Contributor

@yacovm yacovm Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no concrete written plan yet, but I think this interface should not be used for non validators.
I also don't think that the epoch should handle non validator block retrieval, and that we can handle it at a higher layer and have it decoupled from the logic of the epoch.

Right now, we're wiring the epoch object into Simplex despite it not being the most high level component.

config.Log.Warn("Node is not a validator for the subnet",
zap.String("nodeID", config.Ctx.NodeID.String()),
zap.String("chainID", config.Ctx.ChainID.String()),
zap.String("subnetID", config.Ctx.SubnetID.String()),
)
return nil, fmt.Errorf("%w could not find our node: %s", errNodeNotFound, config.Ctx.NodeID)
}

sortNodes(nodes)

c := &Comm{
subnetID: config.Ctx.SubnetID,
nodes: nodes,
nodeID: config.Ctx.NodeID[:],
logger: config.Log,
sender: config.Sender,
msgBuilder: config.OutboundMsgBuilder,
chainID: config.Ctx.ChainID,
}

return c, nil
}

// sortNodes sorts the nodes in place by their byte representations.
func sortNodes(nodes []simplex.NodeID) {
slices.SortFunc(nodes, func(i, j simplex.NodeID) int {
return bytes.Compare(i, j)
})
}

func (c *Comm) ListNodes() []simplex.NodeID {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I feel like the return type makes it obvious that this is a list. Should this just be:

Suggested change
func (c *Comm) ListNodes() []simplex.NodeID {
func (c *Comm) Nodes() []simplex.NodeID {

?

Or perhaps even

Suggested change
func (c *Comm) ListNodes() []simplex.NodeID {
func (c *Comm) Validators() []simplex.NodeID {

return c.nodes
}

func (c *Comm) SendMessage(msg *simplex.Message, destination simplex.NodeID) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
func (c *Comm) SendMessage(msg *simplex.Message, destination simplex.NodeID) {
func (c *Comm) Send(msg *simplex.Message, destination simplex.NodeID) {

Message feels redundant.

outboundMsg, err := c.simplexMessageToOutboundMessage(msg)
if err != nil {
c.logger.Error("Failed creating message", zap.Error(err))
return
}

dest, err := ids.ToNodeID(destination)
if err != nil {
c.logger.Error("Failed to convert destination NodeID", zap.Error(err))
return
}

c.sender.Send(outboundMsg, common.SendConfig{NodeIDs: set.Of(dest)}, c.subnetID, subnets.NoOpAllower)
}

func (c *Comm) Broadcast(msg *simplex.Message) {
for _, node := range c.nodes {
if node.Equals(c.nodeID) {
continue
}

c.SendMessage(msg, node)
}
}

func (c *Comm) simplexMessageToOutboundMessage(msg *simplex.Message) (message.OutboundMessage, error) {
var simplexMsg *p2p.Simplex
switch {
case msg.VerifiedBlockMessage != nil:
bytes, err := msg.VerifiedBlockMessage.VerifiedBlock.Bytes()
if err != nil {
return nil, fmt.Errorf("failed to serialize block: %w", err)
}
simplexMsg = newP2PSimplexBlockProposal(c.chainID, bytes, msg.VerifiedBlockMessage.Vote)
case msg.VoteMessage != nil:
simplexMsg = newP2PSimplexVote(c.chainID, msg.VoteMessage.Vote.BlockHeader, msg.VoteMessage.Signature)
case msg.EmptyVoteMessage != nil:
simplexMsg = newP2PSimplexEmptyVote(c.chainID, msg.EmptyVoteMessage.Vote.ProtocolMetadata, msg.EmptyVoteMessage.Signature)
case msg.FinalizeVote != nil:
simplexMsg = newP2PSimplexFinalizeVote(c.chainID, msg.FinalizeVote.Finalization.BlockHeader, msg.FinalizeVote.Signature)
case msg.Notarization != nil:
simplexMsg = newP2PSimplexNotarization(c.chainID, msg.Notarization.Vote.BlockHeader, msg.Notarization.QC.Bytes())
case msg.EmptyNotarization != nil:
simplexMsg = newP2PSimplexEmptyNotarization(c.chainID, msg.EmptyNotarization.Vote.ProtocolMetadata, msg.EmptyNotarization.QC.Bytes())
case msg.Finalization != nil:
simplexMsg = newP2PSimplexFinalization(c.chainID, msg.Finalization.Finalization.BlockHeader, msg.Finalization.QC.Bytes())
case msg.ReplicationRequest != nil:
simplexMsg = newP2PSimplexReplicationRequest(c.chainID, msg.ReplicationRequest.Seqs, msg.ReplicationRequest.LatestRound)
case msg.VerifiedReplicationResponse != nil:
msg, err := newP2PSimplexReplicationResponse(c.chainID, msg.VerifiedReplicationResponse.Data, msg.VerifiedReplicationResponse.LatestRound)
if err != nil {
return nil, fmt.Errorf("failed to create replication response: %w", err)
}
simplexMsg = msg
}

return c.msgBuilder.SimplexMessage(simplexMsg)
}
106 changes: 106 additions & 0 deletions simplex/comm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package simplex

import (
"testing"
"time"

"github.com/ava-labs/simplex"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"

"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/message"
"github.com/ava-labs/avalanchego/message/messagemock"
"github.com/ava-labs/avalanchego/snow/networking/sender/sendermock"
"github.com/ava-labs/avalanchego/utils/constants"
)

var testSimplexMessage = simplex.Message{
VoteMessage: &simplex.Vote{
Vote: simplex.ToBeSignedVote{
BlockHeader: simplex.BlockHeader{
ProtocolMetadata: simplex.ProtocolMetadata{
Version: 1,
Epoch: 1,
Round: 1,
Seq: 1,
},
},
},
Signature: simplex.Signature{
Signer: []byte("dummy_node_id"),
Value: []byte("dummy_signature"),
},
},
}

func TestCommSendMessage(t *testing.T) {
config := newEngineConfig(t, 1)

destinationNodeID := ids.GenerateTestNodeID()
ctrl := gomock.NewController(t)
sender := sendermock.NewExternalSender(ctrl)
mc, err := message.NewCreator(
prometheus.NewRegistry(),
constants.DefaultNetworkCompressionType,
10*time.Second,
)
require.NoError(t, err)

config.OutboundMsgBuilder = mc
config.Sender = sender

comm, err := NewComm(config)
require.NoError(t, err)

sender.EXPECT().Send(gomock.Any(), gomock.Any(), comm.subnetID, gomock.Any())

comm.SendMessage(&testSimplexMessage, destinationNodeID[:])
}

// TestCommBroadcast tests the Broadcast method sends to all nodes in the subnet
// not including the sending node.
func TestCommBroadcast(t *testing.T) {
config := newEngineConfig(t, 3)

ctrl := gomock.NewController(t)
sender := sendermock.NewExternalSender(ctrl)
mc, err := message.NewCreator(
prometheus.NewRegistry(),
constants.DefaultNetworkCompressionType,
10*time.Second,
)
require.NoError(t, err)

config.OutboundMsgBuilder = mc
config.Sender = sender

comm, err := NewComm(config)
require.NoError(t, err)

sender.EXPECT().Send(gomock.Any(), gomock.Any(), comm.subnetID, gomock.Any()).Times(2)

comm.Broadcast(&testSimplexMessage)
}

func TestCommFailsWithoutCurrentNode(t *testing.T) {
config := newEngineConfig(t, 3)

ctrl := gomock.NewController(t)
msgCreator := messagemock.NewOutboundMsgBuilder(ctrl)
sender := sendermock.NewExternalSender(ctrl)

config.OutboundMsgBuilder = msgCreator
config.Sender = sender

// set the curNode to a different nodeID than the one in the config
vdrs := generateTestNodes(t, 3)
config.Validators = newTestValidatorInfo(vdrs)

_, err := NewComm(config)
require.ErrorIs(t, err, errNodeNotFound)
}
8 changes: 8 additions & 0 deletions simplex/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package simplex

import (
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/message"
"github.com/ava-labs/avalanchego/snow/networking/sender"
"github.com/ava-labs/avalanchego/snow/validators"
"github.com/ava-labs/avalanchego/utils/logging"
)
Expand All @@ -14,6 +16,9 @@ type Config struct {
Ctx SimplexChainContext
Log logging.Logger

Sender sender.ExternalSender
OutboundMsgBuilder message.OutboundMsgBuilder

// Validators is a map of node IDs to their validator information.
// This tells the node about the current membership set, and should be consistent
// across all nodes in the subnet.
Expand All @@ -31,6 +36,9 @@ type SimplexChainContext struct {
// ChainID is the ID of the chain this context exists within.
ChainID ids.ID

// SubnetID is the ID of the subnet this context exists within.
SubnetID ids.ID

// NodeID is the ID of this node
NetworkID uint32
}
Loading