From c0cfff8643b27366165ec2968732a9ff95b15c9e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 13 Nov 2023 19:07:19 -0500 Subject: [PATCH 01/16] Refactor bootstrapper implementation into consensus --- .../snowman/bootstrapper/bootstrapper.go | 21 + .../snowman/bootstrapper/majority.go | 185 +++++ .../snowman/bootstrapper/majority_test.go | 671 ++++++++++++++++++ snow/consensus/snowman/bootstrapper/noop.go | 37 + .../snowman/bootstrapper/noop_test.go | 37 + snow/engine/common/bootstrapper.go | 337 +++------ 6 files changed, 1033 insertions(+), 255 deletions(-) create mode 100644 snow/consensus/snowman/bootstrapper/bootstrapper.go create mode 100644 snow/consensus/snowman/bootstrapper/majority.go create mode 100644 snow/consensus/snowman/bootstrapper/majority_test.go create mode 100644 snow/consensus/snowman/bootstrapper/noop.go create mode 100644 snow/consensus/snowman/bootstrapper/noop_test.go diff --git a/snow/consensus/snowman/bootstrapper/bootstrapper.go b/snow/consensus/snowman/bootstrapper/bootstrapper.go new file mode 100644 index 000000000000..49ca898cf8a5 --- /dev/null +++ b/snow/consensus/snowman/bootstrapper/bootstrapper.go @@ -0,0 +1,21 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapper + +import ( + "context" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/set" +) + +type Bootstrapper interface { + GetAcceptedFrontiersToSend(ctx context.Context) (peers set.Set[ids.NodeID]) + RecordAcceptedFrontier(ctx context.Context, nodeID ids.NodeID, blkIDs ...ids.ID) + GetAcceptedFrontier(ctx context.Context) (blkIDs []ids.ID, finalized bool) + + GetAcceptedToSend(ctx context.Context) (peers set.Set[ids.NodeID]) + RecordAccepted(ctx context.Context, nodeID ids.NodeID, blkIDs []ids.ID) error + GetAccepted(ctx context.Context) (blkIDs []ids.ID, finalized bool) +} diff --git a/snow/consensus/snowman/bootstrapper/majority.go b/snow/consensus/snowman/bootstrapper/majority.go new file mode 100644 index 000000000000..ccef7352a497 --- /dev/null +++ b/snow/consensus/snowman/bootstrapper/majority.go @@ -0,0 +1,185 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapper + +import ( + "context" + + "go.uber.org/zap" + + "golang.org/x/exp/maps" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/utils/sampler" + "github.com/ava-labs/avalanchego/utils/set" +) + +type majority struct { + log logging.Logger + nodeWeights map[ids.NodeID]uint64 + maxOutstanding int + + pendingSendAcceptedFrontier set.Set[ids.NodeID] + outstandingAcceptedFrontier set.Set[ids.NodeID] + receivedAcceptedFrontierSet set.Set[ids.ID] + receivedAcceptedFrontier []ids.ID + + pendingSendAccepted set.Set[ids.NodeID] + outstandingAccepted set.Set[ids.NodeID] + receivedAccepted map[ids.ID]uint64 + accepted []ids.ID +} + +func New( + log logging.Logger, + nodeWeights map[ids.NodeID]uint64, + maxFrontiers int, + maxOutstanding int, +) (Bootstrapper, error) { + nodeIDs := maps.Keys(nodeWeights) + m := &majority{ + log: log, + nodeWeights: nodeWeights, + maxOutstanding: maxOutstanding, + pendingSendAccepted: set.Of(nodeIDs...), + receivedAccepted: make(map[ids.ID]uint64), + } + + maxFrontiers = math.Min(maxFrontiers, len(nodeIDs)) + sampler := sampler.NewUniform() + sampler.Initialize(uint64(len(nodeIDs))) + indicies, err := sampler.Sample(maxFrontiers) + for _, index := range indicies { + m.pendingSendAcceptedFrontier.Add(nodeIDs[index]) + } + + log.Debug("sampled nodes to seed bootstrapping frontier", + zap.Reflect("sampledNodes", m.pendingSendAcceptedFrontier), + zap.Int("numNodes", len(nodeIDs)), + ) + + return m, err +} + +func (m *majority) GetAcceptedFrontiersToSend(context.Context) set.Set[ids.NodeID] { + return getPeersToSend( + &m.pendingSendAcceptedFrontier, + &m.outstandingAcceptedFrontier, + m.maxOutstanding, + ) +} + +func (m *majority) RecordAcceptedFrontier(_ context.Context, nodeID ids.NodeID, blkIDs ...ids.ID) { + if !m.outstandingAcceptedFrontier.Contains(nodeID) { + return + } + + m.outstandingAcceptedFrontier.Remove(nodeID) + m.receivedAcceptedFrontierSet.Add(blkIDs...) + + if !m.finishedFetchingAcceptedFrontiers() { + return + } + + m.receivedAcceptedFrontier = m.receivedAcceptedFrontierSet.List() + + m.log.Debug("finalized bootstrapping frontier", + zap.Stringers("frontier", m.receivedAcceptedFrontier), + ) +} + +func (m *majority) GetAcceptedFrontier(context.Context) ([]ids.ID, bool) { + return m.receivedAcceptedFrontier, m.finishedFetchingAcceptedFrontiers() +} + +func (m *majority) GetAcceptedToSend(context.Context) set.Set[ids.NodeID] { + if !m.finishedFetchingAcceptedFrontiers() { + return nil + } + + return getPeersToSend( + &m.pendingSendAccepted, + &m.outstandingAccepted, + m.maxOutstanding, + ) +} + +func (m *majority) RecordAccepted(_ context.Context, nodeID ids.NodeID, blkIDs []ids.ID) error { + if !m.outstandingAccepted.Contains(nodeID) { + return nil + } + + m.outstandingAccepted.Remove(nodeID) + + weight := m.nodeWeights[nodeID] + for _, blkID := range blkIDs { + newWeight, err := math.Add64(m.receivedAccepted[blkID], weight) + if err != nil { + return err + } + m.receivedAccepted[blkID] = newWeight + } + + if !m.finishedFetchingAccepted() { + return nil + } + + var ( + totalWeight uint64 + err error + ) + for _, weight := range m.nodeWeights { + totalWeight, err = math.Add64(totalWeight, weight) + if err != nil { + return err + } + } + + requiredWeight := totalWeight/2 + 1 + for blkID, weight := range m.receivedAccepted { + if weight >= requiredWeight { + m.accepted = append(m.accepted, blkID) + } + } + + m.log.Debug("finalized bootstrapping instance", + zap.Stringers("accepted", m.accepted), + ) + return nil +} + +func (m *majority) GetAccepted(context.Context) ([]ids.ID, bool) { + return m.accepted, m.finishedFetchingAccepted() +} + +func (m *majority) finishedFetchingAcceptedFrontiers() bool { + return m.pendingSendAcceptedFrontier.Len() == 0 && + m.outstandingAcceptedFrontier.Len() == 0 +} + +func (m *majority) finishedFetchingAccepted() bool { + return m.pendingSendAccepted.Len() == 0 && + m.outstandingAccepted.Len() == 0 +} + +func getPeersToSend(pendingSend, outstanding *set.Set[ids.NodeID], maxOutstanding int) set.Set[ids.NodeID] { + numPending := outstanding.Len() + if numPending >= maxOutstanding { + return nil + } + + numToSend := math.Min( + maxOutstanding-numPending, + pendingSend.Len(), + ) + nodeIDs := set.NewSet[ids.NodeID](numToSend) + for i := 0; i < numToSend; i++ { + nodeID, _ := pendingSend.Pop() + nodeIDs.Add(nodeID) + } + outstanding.Union(nodeIDs) + return nodeIDs +} diff --git a/snow/consensus/snowman/bootstrapper/majority_test.go b/snow/consensus/snowman/bootstrapper/majority_test.go new file mode 100644 index 000000000000..d570c2c85ae7 --- /dev/null +++ b/snow/consensus/snowman/bootstrapper/majority_test.go @@ -0,0 +1,671 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapper + +import ( + "context" + "math" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" + + safemath "github.com/ava-labs/avalanchego/utils/math" +) + +var ( + nodeID0 = ids.GenerateTestNodeID() + nodeID1 = ids.GenerateTestNodeID() + nodeID2 = ids.GenerateTestNodeID() + + blkID0 = ids.GenerateTestID() + blkID1 = ids.GenerateTestID() +) + +func TestNew(t *testing.T) { + require := require.New(t) + + bootstrapper, err := New( + logging.NoLog{}, + map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, // nodeWeights + 2, // maxFrontiers + 2, // maxOutstanding + ) + require.NoError(err) + + expectedBootstrapper := &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 2, + pendingSendAcceptedFrontier: set.Of(nodeID0, nodeID1), + pendingSendAccepted: set.Of(nodeID0, nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + } + require.Equal(expectedBootstrapper, bootstrapper) +} + +func TestNewSampling(t *testing.T) { + require := require.New(t) + + bootstrapper, err := New( + logging.NoLog{}, + map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, // nodeWeights + 1, // maxFrontiers + 2, // maxOutstanding + ) + require.NoError(err) + + peers := bootstrapper.GetAcceptedFrontiersToSend(context.Background()) + require.Len(peers, 1) +} + +func TestNewEmpty(t *testing.T) { + require := require.New(t) + + bootstrapper, err := New( + logging.NoLog{}, + map[ids.NodeID]uint64{}, // nodeWeights + 1, // maxFrontiers + 2, // maxOutstanding + ) + require.NoError(err) + + accepted, finalized := bootstrapper.GetAccepted(context.Background()) + require.Empty(accepted) + require.True(finalized) +} + +func TestMajorityGetAcceptedFrontiersToSend(t *testing.T) { + tests := []struct { + name string + bootstrapper Bootstrapper + expectedState Bootstrapper + expectedPeers set.Set[ids.NodeID] + }{ + { + name: "max outstanding", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + pendingSendAcceptedFrontier: set.Of(nodeID0), + outstandingAcceptedFrontier: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + pendingSendAcceptedFrontier: set.Of(nodeID0), + outstandingAcceptedFrontier: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedPeers: nil, + }, + { + name: "send until max outstanding", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 2, + pendingSendAcceptedFrontier: set.Of(nodeID0, nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 2, + pendingSendAcceptedFrontier: set.Set[ids.NodeID]{}, + outstandingAcceptedFrontier: set.Of(nodeID0, nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedPeers: set.Of(nodeID0, nodeID1), + }, + { + name: "send until no more to send", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + }, + maxOutstanding: 2, + pendingSendAcceptedFrontier: set.Of(nodeID0), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + }, + maxOutstanding: 2, + pendingSendAcceptedFrontier: set.Set[ids.NodeID]{}, + outstandingAcceptedFrontier: set.Of(nodeID0), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedPeers: set.Of(nodeID0), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + peers := test.bootstrapper.GetAcceptedFrontiersToSend(context.Background()) + require.Equal(test.expectedState, test.bootstrapper) + require.Equal(test.expectedPeers, peers) + }) + } +} + +func TestMajorityRecordAcceptedFrontier(t *testing.T) { + tests := []struct { + name string + bootstrapper Bootstrapper + nodeID ids.NodeID + blkIDs []ids.ID + expectedState Bootstrapper + }{ + { + name: "unexpected response", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + pendingSendAcceptedFrontier: set.Of(nodeID0), + outstandingAcceptedFrontier: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + nodeID: nodeID0, + blkIDs: nil, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + pendingSendAcceptedFrontier: set.Of(nodeID0), + outstandingAcceptedFrontier: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + }, + { + name: "unfinished after response", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + pendingSendAcceptedFrontier: set.Of(nodeID0), + outstandingAcceptedFrontier: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + nodeID: nodeID1, + blkIDs: []ids.ID{blkID0}, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + pendingSendAcceptedFrontier: set.Of(nodeID0), + outstandingAcceptedFrontier: set.Set[ids.NodeID]{}, + receivedAcceptedFrontierSet: set.Of(blkID0), + receivedAccepted: make(map[ids.ID]uint64), + }, + }, + { + name: "finished after response", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + outstandingAcceptedFrontier: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + nodeID: nodeID1, + blkIDs: []ids.ID{blkID0}, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + outstandingAcceptedFrontier: set.Set[ids.NodeID]{}, + receivedAcceptedFrontierSet: set.Of(blkID0), + receivedAcceptedFrontier: []ids.ID{blkID0}, + receivedAccepted: make(map[ids.ID]uint64), + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.bootstrapper.RecordAcceptedFrontier(context.Background(), test.nodeID, test.blkIDs...) + require.Equal(t, test.expectedState, test.bootstrapper) + }) + } +} + +func TestMajorityGetAcceptedFrontier(t *testing.T) { + tests := []struct { + name string + bootstrapper Bootstrapper + expectedAcceptedFrontier []ids.ID + expectedFinalized bool + }{ + { + name: "not finalized", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + outstandingAcceptedFrontier: set.Of(nodeID1), + receivedAcceptedFrontier: nil, + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedAcceptedFrontier: nil, + expectedFinalized: false, + }, + { + name: "finalized", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + receivedAcceptedFrontier: []ids.ID{blkID0}, + }, + expectedAcceptedFrontier: []ids.ID{blkID0}, + expectedFinalized: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + acceptedFrontier, finalized := test.bootstrapper.GetAcceptedFrontier(context.Background()) + require.Equal(test.expectedAcceptedFrontier, acceptedFrontier) + require.Equal(test.expectedFinalized, finalized) + }) + } +} + +func TestMajorityGetAcceptedToSend(t *testing.T) { + tests := []struct { + name string + bootstrapper Bootstrapper + expectedState Bootstrapper + expectedPeers set.Set[ids.NodeID] + }{ + { + name: "still fetching frontiers", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + outstandingAcceptedFrontier: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + outstandingAcceptedFrontier: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedPeers: nil, + }, + { + name: "max outstanding", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + pendingSendAccepted: set.Of(nodeID0), + outstandingAccepted: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + pendingSendAccepted: set.Of(nodeID0), + outstandingAccepted: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedPeers: nil, + }, + { + name: "send until max outstanding", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 2, + pendingSendAccepted: set.Of(nodeID0, nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 2, + pendingSendAccepted: set.Set[ids.NodeID]{}, + outstandingAccepted: set.Of(nodeID0, nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedPeers: set.Of(nodeID0, nodeID1), + }, + { + name: "send until no more to send", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + }, + maxOutstanding: 2, + pendingSendAccepted: set.Of(nodeID0), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + }, + maxOutstanding: 2, + pendingSendAccepted: set.Set[ids.NodeID]{}, + outstandingAccepted: set.Of(nodeID0), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedPeers: set.Of(nodeID0), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + peers := test.bootstrapper.GetAcceptedToSend(context.Background()) + require.Equal(test.expectedState, test.bootstrapper) + require.Equal(test.expectedPeers, peers) + }) + } +} + +func TestMajorityRecordAccepted(t *testing.T) { + tests := []struct { + name string + bootstrapper Bootstrapper + nodeID ids.NodeID + blkIDs []ids.ID + expectedState Bootstrapper + expectedErr error + }{ + { + name: "unexpected response", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + pendingSendAccepted: set.Of(nodeID0), + outstandingAccepted: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + nodeID: nodeID0, + blkIDs: nil, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + pendingSendAccepted: set.Of(nodeID0), + outstandingAccepted: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + expectedErr: nil, + }, + { + name: "unfinished after response", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 2, + nodeID1: 3, + }, + maxOutstanding: 1, + pendingSendAccepted: set.Of(nodeID0), + outstandingAccepted: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + nodeID: nodeID1, + blkIDs: []ids.ID{blkID0}, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 2, + nodeID1: 3, + }, + maxOutstanding: 1, + pendingSendAccepted: set.Of(nodeID0), + outstandingAccepted: set.Set[ids.NodeID]{}, + receivedAccepted: map[ids.ID]uint64{ + blkID0: 3, + }, + }, + expectedErr: nil, + }, + { + name: "overflow during response", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: math.MaxUint64, + }, + maxOutstanding: 1, + outstandingAccepted: set.Of(nodeID1), + receivedAccepted: map[ids.ID]uint64{ + blkID0: 1, + }, + }, + nodeID: nodeID1, + blkIDs: []ids.ID{blkID0}, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: math.MaxUint64, + }, + maxOutstanding: 1, + outstandingAccepted: set.Set[ids.NodeID]{}, + receivedAccepted: map[ids.ID]uint64{ + blkID0: 1, + }, + }, + expectedErr: safemath.ErrOverflow, + }, + { + name: "overflow during final response", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: math.MaxUint64, + }, + maxOutstanding: 1, + outstandingAccepted: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + }, + nodeID: nodeID1, + blkIDs: []ids.ID{blkID0}, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: math.MaxUint64, + }, + maxOutstanding: 1, + outstandingAccepted: set.Set[ids.NodeID]{}, + receivedAccepted: map[ids.ID]uint64{ + blkID0: math.MaxUint64, + }, + }, + expectedErr: safemath.ErrOverflow, + }, + { + name: "finished after response", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + nodeID2: 1, + }, + maxOutstanding: 1, + outstandingAccepted: set.Of(nodeID2), + receivedAccepted: map[ids.ID]uint64{ + blkID0: 1, + blkID1: 1, + }, + }, + nodeID: nodeID2, + blkIDs: []ids.ID{blkID1}, + expectedState: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + nodeID2: 1, + }, + maxOutstanding: 1, + outstandingAccepted: set.Set[ids.NodeID]{}, + receivedAccepted: map[ids.ID]uint64{ + blkID0: 1, + blkID1: 2, + }, + accepted: []ids.ID{blkID1}, + }, + expectedErr: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + err := test.bootstrapper.RecordAccepted(context.Background(), test.nodeID, test.blkIDs) + require.Equal(test.expectedState, test.bootstrapper) + require.ErrorIs(err, test.expectedErr) + }) + } +} + +func TestMajorityGetAccepted(t *testing.T) { + tests := []struct { + name string + bootstrapper Bootstrapper + expectedAccepted []ids.ID + expectedFinalized bool + }{ + { + name: "not finalized", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + outstandingAccepted: set.Of(nodeID1), + receivedAccepted: make(map[ids.ID]uint64), + accepted: nil, + }, + expectedAccepted: nil, + expectedFinalized: false, + }, + { + name: "finalized", + bootstrapper: &majority{ + log: logging.NoLog{}, + nodeWeights: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + maxOutstanding: 1, + receivedAccepted: map[ids.ID]uint64{ + blkID0: 2, + }, + accepted: []ids.ID{blkID0}, + }, + expectedAccepted: []ids.ID{blkID0}, + expectedFinalized: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + accepted, finalized := test.bootstrapper.GetAccepted(context.Background()) + require.Equal(test.expectedAccepted, accepted) + require.Equal(test.expectedFinalized, finalized) + }) + } +} diff --git a/snow/consensus/snowman/bootstrapper/noop.go b/snow/consensus/snowman/bootstrapper/noop.go new file mode 100644 index 000000000000..b5ff2d410ba2 --- /dev/null +++ b/snow/consensus/snowman/bootstrapper/noop.go @@ -0,0 +1,37 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapper + +import ( + "context" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/set" +) + +var Noop Bootstrapper = noop{} + +type noop struct{} + +func (noop) GetAcceptedFrontiersToSend(context.Context) set.Set[ids.NodeID] { + return nil +} + +func (noop) RecordAcceptedFrontier(context.Context, ids.NodeID, ...ids.ID) {} + +func (noop) GetAcceptedFrontier(context.Context) ([]ids.ID, bool) { + return nil, false +} + +func (noop) GetAcceptedToSend(context.Context) set.Set[ids.NodeID] { + return nil +} + +func (noop) RecordAccepted(context.Context, ids.NodeID, []ids.ID) error { + return nil +} + +func (noop) GetAccepted(context.Context) ([]ids.ID, bool) { + return nil, false +} diff --git a/snow/consensus/snowman/bootstrapper/noop_test.go b/snow/consensus/snowman/bootstrapper/noop_test.go new file mode 100644 index 000000000000..96f312465c63 --- /dev/null +++ b/snow/consensus/snowman/bootstrapper/noop_test.go @@ -0,0 +1,37 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapper + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" +) + +func TestNoop(t *testing.T) { + var ( + require = require.New(t) + ctx = context.Background() + nodeID = ids.GenerateTestNodeID() + ) + + require.Empty(Noop.GetAcceptedFrontiersToSend(ctx)) + + Noop.RecordAcceptedFrontier(ctx, nodeID) + + blkIDs, finalized := Noop.GetAcceptedFrontier(ctx) + require.Empty(blkIDs) + require.False(finalized) + + require.Empty(Noop.GetAcceptedToSend(ctx)) + + require.NoError(Noop.RecordAccepted(ctx, nodeID, nil)) + + blkIDs, finalized = Noop.GetAccepted(ctx) + require.Empty(blkIDs) + require.False(finalized) +} diff --git a/snow/engine/common/bootstrapper.go b/snow/engine/common/bootstrapper.go index aca219130478..4f90ca6d9a6b 100644 --- a/snow/engine/common/bootstrapper.go +++ b/snow/engine/common/bootstrapper.go @@ -5,16 +5,12 @@ package common import ( "context" - "fmt" - "math" "go.uber.org/zap" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/validators" - "github.com/ava-labs/avalanchego/utils/set" - safemath "github.com/ava-labs/avalanchego/utils/math" + smbootstrapper "github.com/ava-labs/avalanchego/snow/consensus/snowman/bootstrapper" ) const ( @@ -46,30 +42,7 @@ type bootstrapper struct { Config Halter - // Holds the beacons that were sampled for the accepted frontier - sampledBeacons validators.Manager - // IDs of validators we should request an accepted frontier from - pendingSendAcceptedFrontier set.Set[ids.NodeID] - // IDs of validators we requested an accepted frontier from but haven't - // received a reply yet - pendingReceiveAcceptedFrontier set.Set[ids.NodeID] - // IDs of validators that failed to respond with their accepted frontier - failedAcceptedFrontier set.Set[ids.NodeID] - // IDs of all the returned accepted frontiers - acceptedFrontierSet set.Set[ids.ID] - - // IDs of validators we should request filtering the accepted frontier from - pendingSendAccepted set.Set[ids.NodeID] - // IDs of validators we requested filtering the accepted frontier from but - // haven't received a reply yet - pendingReceiveAccepted set.Set[ids.NodeID] - // IDs of validators that failed to respond with their filtered accepted - // frontier - failedAccepted set.Set[ids.NodeID] - // IDs of the returned accepted containers and the stake weight that has - // marked them as accepted - acceptedVotes map[ids.ID]uint64 - acceptedFrontier []ids.ID + bootstrapper smbootstrapper.Bootstrapper // number of times the bootstrap has been attempted bootstrapAttempts int @@ -77,12 +50,12 @@ type bootstrapper struct { func NewCommonBootstrapper(config Config) Bootstrapper { return &bootstrapper{ - Config: config, + Config: config, + bootstrapper: smbootstrapper.Noop, } } func (b *bootstrapper) AcceptedFrontier(ctx context.Context, nodeID ids.NodeID, requestID uint32, containerID ids.ID) error { - // ignores any late responses if requestID != b.Config.SharedCfg.RequestID { b.Ctx.Log.Debug("received out-of-sync AcceptedFrontier message", zap.Stringer("nodeID", nodeID), @@ -92,21 +65,11 @@ func (b *bootstrapper) AcceptedFrontier(ctx context.Context, nodeID ids.NodeID, return nil } - if !b.pendingReceiveAcceptedFrontier.Contains(nodeID) { - b.Ctx.Log.Debug("received unexpected AcceptedFrontier message", - zap.Stringer("nodeID", nodeID), - ) - return nil - } - - // Union the reported accepted frontier from [nodeID] with the accepted - // frontier we got from others - b.acceptedFrontierSet.Add(containerID) - return b.markAcceptedFrontierReceived(ctx, nodeID) + b.bootstrapper.RecordAcceptedFrontier(ctx, nodeID, containerID) + return b.receivedAcceptedFrontier(ctx) } func (b *bootstrapper) GetAcceptedFrontierFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32) error { - // ignores any late responses if requestID != b.Config.SharedCfg.RequestID { b.Ctx.Log.Debug("received out-of-sync GetAcceptedFrontierFailed message", zap.Stringer("nodeID", nodeID), @@ -116,76 +79,11 @@ func (b *bootstrapper) GetAcceptedFrontierFailed(ctx context.Context, nodeID ids return nil } - if !b.pendingReceiveAcceptedFrontier.Contains(nodeID) { - b.Ctx.Log.Debug("received unexpected GetAcceptedFrontierFailed message", - zap.Stringer("nodeID", nodeID), - ) - return nil - } - - // If we can't get a response from [nodeID], act as though they said their - // accepted frontier is empty and we add the validator to the failed list - b.failedAcceptedFrontier.Add(nodeID) - return b.markAcceptedFrontierReceived(ctx, nodeID) -} - -func (b *bootstrapper) markAcceptedFrontierReceived(ctx context.Context, nodeID ids.NodeID) error { - // Mark that we received a response from [nodeID] - b.pendingReceiveAcceptedFrontier.Remove(nodeID) - - b.sendGetAcceptedFrontiers(ctx) - - // still waiting on requests - if b.pendingReceiveAcceptedFrontier.Len() != 0 { - return nil - } - - // We've received the accepted frontier from every bootstrap validator - // Ask each bootstrap validator to filter the list of containers that we were - // told are on the accepted frontier such that the list only contains containers - // they think are accepted. - totalSampledWeight, err := b.sampledBeacons.TotalWeight(b.Ctx.SubnetID) - if err != nil { - return fmt.Errorf("failed to get total weight of sampled beacons for subnet %s: %w", b.Ctx.SubnetID, err) - } - beaconsTotalWeight, err := b.Beacons.TotalWeight(b.Ctx.SubnetID) - if err != nil { - return fmt.Errorf("failed to get total weight of beacons for subnet %s: %w", b.Ctx.SubnetID, err) - } - newAlpha := float64(totalSampledWeight*b.Alpha) / float64(beaconsTotalWeight) - - failedBeaconWeight, err := b.Beacons.SubsetWeight(b.Ctx.SubnetID, b.failedAcceptedFrontier) - if err != nil { - return fmt.Errorf("failed to get total weight of failed beacons: %w", err) - } - - // fail the bootstrap if the weight is not enough to bootstrap - if float64(totalSampledWeight)-newAlpha < float64(failedBeaconWeight) { - if b.Config.RetryBootstrap { - b.Ctx.Log.Debug("restarting bootstrap", - zap.String("reason", "not enough frontiers received"), - zap.Int("numBeacons", b.Beacons.Count(b.Ctx.SubnetID)), - zap.Int("numFailedBootstrappers", b.failedAcceptedFrontier.Len()), - zap.Int("numBootstrapAttemps", b.bootstrapAttempts), - ) - return b.Restart(ctx, false) - } - - b.Ctx.Log.Debug("didn't receive enough frontiers", - zap.Int("numFailedValidators", b.failedAcceptedFrontier.Len()), - zap.Int("numBootstrapAttempts", b.bootstrapAttempts), - ) - } - - b.Config.SharedCfg.RequestID++ - b.acceptedFrontier = b.acceptedFrontierSet.List() - - b.sendGetAccepted(ctx) - return nil + b.bootstrapper.RecordAcceptedFrontier(ctx, nodeID) + return b.receivedAcceptedFrontier(ctx) } func (b *bootstrapper) Accepted(ctx context.Context, nodeID ids.NodeID, requestID uint32, containerIDs []ids.ID) error { - // ignores any late responses if requestID != b.Config.SharedCfg.RequestID { b.Ctx.Log.Debug("received out-of-sync Accepted message", zap.Stringer("nodeID", nodeID), @@ -195,90 +93,13 @@ func (b *bootstrapper) Accepted(ctx context.Context, nodeID ids.NodeID, requestI return nil } - if !b.pendingReceiveAccepted.Contains(nodeID) { - b.Ctx.Log.Debug("received unexpected Accepted message", - zap.Stringer("nodeID", nodeID), - ) - return nil - } - // Mark that we received a response from [nodeID] - b.pendingReceiveAccepted.Remove(nodeID) - - weight := b.Beacons.GetWeight(b.Ctx.SubnetID, nodeID) - for _, containerID := range containerIDs { - previousWeight := b.acceptedVotes[containerID] - newWeight, err := safemath.Add64(weight, previousWeight) - if err != nil { - b.Ctx.Log.Error("failed calculating the Accepted votes", - zap.Uint64("weight", weight), - zap.Uint64("previousWeight", previousWeight), - zap.Error(err), - ) - newWeight = math.MaxUint64 - } - b.acceptedVotes[containerID] = newWeight - } - - b.sendGetAccepted(ctx) - - // wait on pending responses - if b.pendingReceiveAccepted.Len() != 0 { - return nil - } - - // We've received the filtered accepted frontier from every bootstrap validator - // Accept all containers that have a sufficient weight behind them - accepted := make([]ids.ID, 0, len(b.acceptedVotes)) - for containerID, weight := range b.acceptedVotes { - if weight >= b.Alpha { - accepted = append(accepted, containerID) - } - } - - // if we don't have enough weight for the bootstrap to be accepted then - // retry or fail the bootstrap - size := len(accepted) - if size == 0 && b.Beacons.Count(b.Ctx.SubnetID) > 0 { - // if we had too many timeouts when asking for validator votes, we - // should restart bootstrap hoping for the network problems to go away; - // otherwise, we received enough (>= b.Alpha) responses, but no frontier - // was supported by a majority of validators (i.e. votes are split - // between minorities supporting different frontiers). - beaconTotalWeight, err := b.Beacons.TotalWeight(b.Ctx.SubnetID) - if err != nil { - return fmt.Errorf("failed to get total weight of beacons for subnet %s: %w", b.Ctx.SubnetID, err) - } - failedBeaconWeight, err := b.Beacons.SubsetWeight(b.Ctx.SubnetID, b.failedAccepted) - if err != nil { - return fmt.Errorf("failed to get total weight of failed beacons for subnet %s: %w", b.Ctx.SubnetID, err) - } - votingStakes := beaconTotalWeight - failedBeaconWeight - if b.Config.RetryBootstrap && votingStakes < b.Alpha { - b.Ctx.Log.Debug("restarting bootstrap", - zap.String("reason", "not enough votes received"), - zap.Int("numBeacons", b.Beacons.Count(b.Ctx.SubnetID)), - zap.Int("numFailedBootstrappers", b.failedAccepted.Len()), - zap.Int("numBootstrapAttempts", b.bootstrapAttempts), - ) - return b.Restart(ctx, false) - } - } - - if !b.Config.SharedCfg.Restarted { - b.Ctx.Log.Info("bootstrapping started syncing", - zap.Int("numVerticesInFrontier", size), - ) - } else { - b.Ctx.Log.Debug("bootstrapping started syncing", - zap.Int("numVerticesInFrontier", size), - ) + if err := b.bootstrapper.RecordAccepted(ctx, nodeID, containerIDs); err != nil { + return err } - - return b.Bootstrapable.ForceAccepted(ctx, accepted) + return b.receivedAccepted(ctx) } func (b *bootstrapper) GetAcceptedFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32) error { - // ignores any late responses if requestID != b.Config.SharedCfg.RequestID { b.Ctx.Log.Debug("received out-of-sync GetAcceptedFailed message", zap.Stringer("nodeID", nodeID), @@ -288,58 +109,40 @@ func (b *bootstrapper) GetAcceptedFailed(ctx context.Context, nodeID ids.NodeID, return nil } - // If we can't get a response from [nodeID], act as though they said that - // they think none of the containers we sent them in GetAccepted are - // accepted - b.failedAccepted.Add(nodeID) - return b.Accepted(ctx, nodeID, requestID, nil) + if err := b.bootstrapper.RecordAccepted(ctx, nodeID, nil); err != nil { + return err + } + return b.receivedAccepted(ctx) } func (b *bootstrapper) Startup(ctx context.Context) error { - beaconIDs, err := b.Beacons.Sample(b.Ctx.SubnetID, b.Config.SampleK) + currentBeacons := b.Beacons.GetMap(b.Ctx.SubnetID) + nodeWeights := make(map[ids.NodeID]uint64, len(currentBeacons)) + for nodeID, beacon := range currentBeacons { + nodeWeights[nodeID] = beacon.Weight + } + + bootstrapper, err := smbootstrapper.New( + b.Ctx.Log, + nodeWeights, + b.Config.SampleK, + MaxOutstandingBroadcastRequests, + ) if err != nil { return err } - - b.sampledBeacons = validators.NewManager() - b.pendingSendAcceptedFrontier.Clear() - for _, nodeID := range beaconIDs { - if _, ok := b.sampledBeacons.GetValidator(b.Ctx.SubnetID, nodeID); !ok { - // Invariant: We never use the TxID or BLS keys populated here. - err = b.sampledBeacons.AddStaker(b.Ctx.SubnetID, nodeID, nil, ids.Empty, 1) - } else { - err = b.sampledBeacons.AddWeight(b.Ctx.SubnetID, nodeID, 1) - } - if err != nil { - return err - } - b.pendingSendAcceptedFrontier.Add(nodeID) - } - - b.pendingReceiveAcceptedFrontier.Clear() - b.failedAcceptedFrontier.Clear() - b.acceptedFrontierSet.Clear() - - b.pendingSendAccepted.Clear() - for _, nodeID := range b.Beacons.GetValidatorIDs(b.Ctx.SubnetID) { - b.pendingSendAccepted.Add(nodeID) - } - - b.pendingReceiveAccepted.Clear() - b.failedAccepted.Clear() - b.acceptedVotes = make(map[ids.ID]uint64) + b.bootstrapper = bootstrapper b.bootstrapAttempts++ - if b.pendingSendAcceptedFrontier.Len() == 0 { + if accepted, finalized := b.bootstrapper.GetAccepted(ctx); finalized { b.Ctx.Log.Info("bootstrapping skipped", zap.String("reason", "no provided bootstraps"), ) - return b.Bootstrapable.ForceAccepted(ctx, nil) + return b.Bootstrapable.ForceAccepted(ctx, accepted) } b.Config.SharedCfg.RequestID++ - b.sendGetAcceptedFrontiers(ctx) - return nil + return b.receivedAcceptedFrontier(ctx) } func (b *bootstrapper) Restart(ctx context.Context, reset bool) error { @@ -361,40 +164,64 @@ func (b *bootstrapper) Restart(ctx context.Context, reset bool) error { return b.Startup(ctx) } -// Ask up to [MaxOutstandingBroadcastRequests] bootstrap validators to send -// their accepted frontier with the current accepted frontier -func (b *bootstrapper) sendGetAcceptedFrontiers(ctx context.Context) { - vdrs := set.NewSet[ids.NodeID](1) - for b.pendingSendAcceptedFrontier.Len() > 0 && b.pendingReceiveAcceptedFrontier.Len() < MaxOutstandingBroadcastRequests { - vdr, _ := b.pendingSendAcceptedFrontier.Pop() - // Add the validator to the set to send the messages to - vdrs.Add(vdr) - // Add the validator to send pending receipt set - b.pendingReceiveAcceptedFrontier.Add(vdr) +func (b *bootstrapper) receivedAcceptedFrontier(ctx context.Context) error { + peers := b.bootstrapper.GetAcceptedFrontiersToSend(ctx) + if peers.Len() > 0 { + b.Sender.SendGetAcceptedFrontier(ctx, peers, b.Config.SharedCfg.RequestID) + return nil } - if vdrs.Len() > 0 { - b.Sender.SendGetAcceptedFrontier(ctx, vdrs, b.Config.SharedCfg.RequestID) + // We haven't finalized the accepted frontier, so we should wait for the + // outstanding requests. + _, finalized := b.bootstrapper.GetAcceptedFrontier(ctx) + if !finalized { + return nil } + + b.Config.SharedCfg.RequestID++ + return b.receivedAccepted(ctx) } -// Ask up to [MaxOutstandingBroadcastRequests] bootstrap validators to send -// their filtered accepted frontier -func (b *bootstrapper) sendGetAccepted(ctx context.Context) { - vdrs := set.NewSet[ids.NodeID](1) - for b.pendingSendAccepted.Len() > 0 && b.pendingReceiveAccepted.Len() < MaxOutstandingBroadcastRequests { - vdr, _ := b.pendingSendAccepted.Pop() - // Add the validator to the set to send the messages to - vdrs.Add(vdr) - // Add the validator to send pending receipt set - b.pendingReceiveAccepted.Add(vdr) +func (b *bootstrapper) receivedAccepted(ctx context.Context) error { + potentialAccepted, finalized := b.bootstrapper.GetAcceptedFrontier(ctx) + if !finalized { + // We should never receive an accepted message when the frontier isn't + // finalized, as we should have never sent any GetAccepted messages + // before the frontier is finalized. + b.Ctx.Log.Error("bootstrapping frontier unexpectedly not finalized") + return nil } - if vdrs.Len() > 0 { - b.Ctx.Log.Debug("sent GetAccepted messages", - zap.Int("numSent", vdrs.Len()), - zap.Int("numPending", b.pendingSendAccepted.Len()), + peers := b.bootstrapper.GetAcceptedToSend(ctx) + if peers.Len() > 0 { + b.Sender.SendGetAccepted(ctx, peers, b.Config.SharedCfg.RequestID, potentialAccepted) + return nil + } + + accepted, finalized := b.bootstrapper.GetAccepted(ctx) + if !finalized { + return nil + } + + numAccepted := len(accepted) + if numAccepted == 0 { + b.Ctx.Log.Debug("restarting bootstrap", + zap.String("reason", "no blocks accepted"), + zap.Int("numBeacons", b.Beacons.Count(b.Ctx.SubnetID)), + zap.Int("numBootstrapAttempts", b.bootstrapAttempts), ) - b.Sender.SendGetAccepted(ctx, vdrs, b.Config.SharedCfg.RequestID, b.acceptedFrontier) + return b.Restart(ctx, false) } + + if !b.Config.SharedCfg.Restarted { + b.Ctx.Log.Info("bootstrapping started syncing", + zap.Int("numAccepted", numAccepted), + ) + } else { + b.Ctx.Log.Debug("bootstrapping started syncing", + zap.Int("numAccepted", numAccepted), + ) + } + + return b.Bootstrapable.ForceAccepted(ctx, accepted) } From c1c84a6788a6587394439762ac079fc324976fda Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 14 Nov 2023 11:10:43 -0500 Subject: [PATCH 02/16] Add comment --- .../snowman/bootstrapper/bootstrapper.go | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/snow/consensus/snowman/bootstrapper/bootstrapper.go b/snow/consensus/snowman/bootstrapper/bootstrapper.go index 49ca898cf8a5..f607da93dafd 100644 --- a/snow/consensus/snowman/bootstrapper/bootstrapper.go +++ b/snow/consensus/snowman/bootstrapper/bootstrapper.go @@ -10,12 +10,47 @@ import ( "github.com/ava-labs/avalanchego/utils/set" ) +// Bootstrapper implements the protocol used to determine the initial set of +// accepted blocks to sync to. +// +// The bootstrapping protocol starts by fetching the last accepted block from an +// initial subset of peers. In order for the protocol to find a recently +// accepted block, there must be at least one correct node in this subset of +// peers. If there is not a correct node in the subset of peers, the node will +// not accept an incorrect block. However, the node may be unable to find an +// acceptable block. +// +// Once the last accepted blocks have been fetched from the subset of peers, the +// set of blocks are sent to all peers. Each peer is expected to filter the +// provided blocks and report which of them they consider accepted. If a +// majority of the peers report that a block is accepted, then the node will +// consider that block to be accepted by the network. This assumes that a +// majority of the network is correct. If a majority of the network is +// malicious, the node may accept an incorrect block. type Bootstrapper interface { + // GetAcceptedFrontiersToSend returns the set of peers whose accepted + // frontier should be requested. It is expected to repeatedly call this + // function along with [RecordAcceptedFrontier] until [GetAcceptedFrontier] + // returns that the frontier is finalized. GetAcceptedFrontiersToSend(ctx context.Context) (peers set.Set[ids.NodeID]) + // RecordAcceptedFrontier of nodes whose accepted frontiers were requested. + // [blkIDs] is typically either empty, if the request for frontiers failed, + // or a single block. RecordAcceptedFrontier(ctx context.Context, nodeID ids.NodeID, blkIDs ...ids.ID) + // GetAcceptedFrontier returns the union of all the provided frontiers along + // with a flag to identify that the frontier has finished being calculated. GetAcceptedFrontier(ctx context.Context) (blkIDs []ids.ID, finalized bool) + // GetAcceptedFrontiersToSend returns the set of peers who should be + // requested to filter the frontier. It is expected to repeatedly call this + // function along with [RecordAccepted] until [GetAccepted] returns that the + // set is finalized. GetAcceptedToSend(ctx context.Context) (peers set.Set[ids.NodeID]) + // RecordAccepted blocks of nodes that were requested. [blkIDs] should + // typically be a subset of the frontier. Any returned error should be + // treated as fatal. RecordAccepted(ctx context.Context, nodeID ids.NodeID, blkIDs []ids.ID) error + // GetAccepted returns a set of accepted blocks along with a flag to + // identify that the set has finished being calculated. GetAccepted(ctx context.Context) (blkIDs []ids.ID, finalized bool) } From b0db18109d46da6946bc98cb64e24c968e2d455d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 14 Nov 2023 11:28:26 -0500 Subject: [PATCH 03/16] Add logs --- snow/consensus/snowman/bootstrapper/majority.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/snow/consensus/snowman/bootstrapper/majority.go b/snow/consensus/snowman/bootstrapper/majority.go index ccef7352a497..4649ceb7d40b 100644 --- a/snow/consensus/snowman/bootstrapper/majority.go +++ b/snow/consensus/snowman/bootstrapper/majority.go @@ -11,6 +11,7 @@ import ( "golang.org/x/exp/maps" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/message" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/utils/sampler" @@ -74,6 +75,11 @@ func (m *majority) GetAcceptedFrontiersToSend(context.Context) set.Set[ids.NodeI func (m *majority) RecordAcceptedFrontier(_ context.Context, nodeID ids.NodeID, blkIDs ...ids.ID) { if !m.outstandingAcceptedFrontier.Contains(nodeID) { + m.log.Error("received unexpected message", + zap.Stringer("messageOp", message.AcceptedFrontierOp), + zap.Stringer("nodeID", nodeID), + zap.Stringers("blkIDs", blkIDs), + ) return } @@ -109,6 +115,11 @@ func (m *majority) GetAcceptedToSend(context.Context) set.Set[ids.NodeID] { func (m *majority) RecordAccepted(_ context.Context, nodeID ids.NodeID, blkIDs []ids.ID) error { if !m.outstandingAccepted.Contains(nodeID) { + m.log.Error("received unexpected message", + zap.Stringer("messageOp", message.AcceptedOp), + zap.Stringer("nodeID", nodeID), + zap.Stringers("blkIDs", blkIDs), + ) return nil } From 940b1952316bd288566496d08425a0b10fc27ed1 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 14 Nov 2023 11:34:16 -0500 Subject: [PATCH 04/16] add comment --- snow/consensus/snowman/bootstrapper/majority.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/snow/consensus/snowman/bootstrapper/majority.go b/snow/consensus/snowman/bootstrapper/majority.go index 4649ceb7d40b..19d05bbd5d38 100644 --- a/snow/consensus/snowman/bootstrapper/majority.go +++ b/snow/consensus/snowman/bootstrapper/majority.go @@ -75,6 +75,7 @@ func (m *majority) GetAcceptedFrontiersToSend(context.Context) set.Set[ids.NodeI func (m *majority) RecordAcceptedFrontier(_ context.Context, nodeID ids.NodeID, blkIDs ...ids.ID) { if !m.outstandingAcceptedFrontier.Contains(nodeID) { + // The chain router should have already dropped unexpected messages. m.log.Error("received unexpected message", zap.Stringer("messageOp", message.AcceptedFrontierOp), zap.Stringer("nodeID", nodeID), @@ -115,6 +116,7 @@ func (m *majority) GetAcceptedToSend(context.Context) set.Set[ids.NodeID] { func (m *majority) RecordAccepted(_ context.Context, nodeID ids.NodeID, blkIDs []ids.ID) error { if !m.outstandingAccepted.Contains(nodeID) { + // The chain router should have already dropped unexpected messages. m.log.Error("received unexpected message", zap.Stringer("messageOp", message.AcceptedOp), zap.Stringer("nodeID", nodeID), From d13664d6356e475f657c38e9fa140cb4388eaa09 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 14 Nov 2023 16:14:23 -0500 Subject: [PATCH 05/16] Sample beacons by stake --- .../snowman/bootstrapper/majority.go | 34 +++------ .../snowman/bootstrapper/majority_test.go | 47 ++---------- .../consensus/snowman/bootstrapper/sampler.go | 46 ++++++++++++ .../snowman/bootstrapper/sampler_test.go | 75 +++++++++++++++++++ snow/engine/common/bootstrapper.go | 18 +++-- 5 files changed, 147 insertions(+), 73 deletions(-) create mode 100644 snow/consensus/snowman/bootstrapper/sampler.go create mode 100644 snow/consensus/snowman/bootstrapper/sampler_test.go diff --git a/snow/consensus/snowman/bootstrapper/majority.go b/snow/consensus/snowman/bootstrapper/majority.go index 19d05bbd5d38..771889e36c40 100644 --- a/snow/consensus/snowman/bootstrapper/majority.go +++ b/snow/consensus/snowman/bootstrapper/majority.go @@ -14,7 +14,6 @@ import ( "github.com/ava-labs/avalanchego/message" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/math" - "github.com/ava-labs/avalanchego/utils/sampler" "github.com/ava-labs/avalanchego/utils/set" ) @@ -36,33 +35,18 @@ type majority struct { func New( log logging.Logger, + frontierNodes set.Set[ids.NodeID], nodeWeights map[ids.NodeID]uint64, - maxFrontiers int, maxOutstanding int, -) (Bootstrapper, error) { - nodeIDs := maps.Keys(nodeWeights) - m := &majority{ - log: log, - nodeWeights: nodeWeights, - maxOutstanding: maxOutstanding, - pendingSendAccepted: set.Of(nodeIDs...), - receivedAccepted: make(map[ids.ID]uint64), +) Bootstrapper { + return &majority{ + log: log, + nodeWeights: nodeWeights, + maxOutstanding: maxOutstanding, + pendingSendAcceptedFrontier: frontierNodes, + pendingSendAccepted: set.Of(maps.Keys(nodeWeights)...), + receivedAccepted: make(map[ids.ID]uint64), } - - maxFrontiers = math.Min(maxFrontiers, len(nodeIDs)) - sampler := sampler.NewUniform() - sampler.Initialize(uint64(len(nodeIDs))) - indicies, err := sampler.Sample(maxFrontiers) - for _, index := range indicies { - m.pendingSendAcceptedFrontier.Add(nodeIDs[index]) - } - - log.Debug("sampled nodes to seed bootstrapping frontier", - zap.Reflect("sampledNodes", m.pendingSendAcceptedFrontier), - zap.Int("numNodes", len(nodeIDs)), - ) - - return m, err } func (m *majority) GetAcceptedFrontiersToSend(context.Context) set.Set[ids.NodeID] { diff --git a/snow/consensus/snowman/bootstrapper/majority_test.go b/snow/consensus/snowman/bootstrapper/majority_test.go index d570c2c85ae7..ddc0481ee14f 100644 --- a/snow/consensus/snowman/bootstrapper/majority_test.go +++ b/snow/consensus/snowman/bootstrapper/majority_test.go @@ -27,18 +27,15 @@ var ( ) func TestNew(t *testing.T) { - require := require.New(t) - - bootstrapper, err := New( - logging.NoLog{}, + bootstrapper := New( + logging.NoLog{}, // log + set.Of(nodeID0), // frontierNodes map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: 1, }, // nodeWeights - 2, // maxFrontiers 2, // maxOutstanding ) - require.NoError(err) expectedBootstrapper := &majority{ log: logging.NoLog{}, @@ -47,45 +44,11 @@ func TestNew(t *testing.T) { nodeID1: 1, }, maxOutstanding: 2, - pendingSendAcceptedFrontier: set.Of(nodeID0, nodeID1), + pendingSendAcceptedFrontier: set.Of(nodeID0), pendingSendAccepted: set.Of(nodeID0, nodeID1), receivedAccepted: make(map[ids.ID]uint64), } - require.Equal(expectedBootstrapper, bootstrapper) -} - -func TestNewSampling(t *testing.T) { - require := require.New(t) - - bootstrapper, err := New( - logging.NoLog{}, - map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, - }, // nodeWeights - 1, // maxFrontiers - 2, // maxOutstanding - ) - require.NoError(err) - - peers := bootstrapper.GetAcceptedFrontiersToSend(context.Background()) - require.Len(peers, 1) -} - -func TestNewEmpty(t *testing.T) { - require := require.New(t) - - bootstrapper, err := New( - logging.NoLog{}, - map[ids.NodeID]uint64{}, // nodeWeights - 1, // maxFrontiers - 2, // maxOutstanding - ) - require.NoError(err) - - accepted, finalized := bootstrapper.GetAccepted(context.Background()) - require.Empty(accepted) - require.True(finalized) + require.Equal(t, expectedBootstrapper, bootstrapper) } func TestMajorityGetAcceptedFrontiersToSend(t *testing.T) { diff --git a/snow/consensus/snowman/bootstrapper/sampler.go b/snow/consensus/snowman/bootstrapper/sampler.go new file mode 100644 index 000000000000..a02543c152a3 --- /dev/null +++ b/snow/consensus/snowman/bootstrapper/sampler.go @@ -0,0 +1,46 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapper + +import ( + "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/utils/sampler" + "github.com/ava-labs/avalanchego/utils/set" +) + +func Sample[T comparable](elements map[T]uint64, size int) (set.Set[T], error) { + var ( + keys = make([]T, len(elements)) + weights = make([]uint64, len(elements)) + totalWeight uint64 + err error + ) + i := 0 + for key, weight := range elements { + keys[i] = key + weights[i] = weight + totalWeight, err = math.Add64(totalWeight, weight) + if err != nil { + return nil, err + } + i++ + } + + sampler := sampler.NewWeightedWithoutReplacement() + if err := sampler.Initialize(weights); err != nil { + return nil, err + } + + size = int(math.Min(uint64(size), totalWeight)) + indicies, err := sampler.Sample(size) + if err != nil { + return nil, err + } + + sampledElements := set.NewSet[T](size) + for _, index := range indicies { + sampledElements.Add(keys[index]) + } + return sampledElements, nil +} diff --git a/snow/consensus/snowman/bootstrapper/sampler_test.go b/snow/consensus/snowman/bootstrapper/sampler_test.go new file mode 100644 index 000000000000..34cb41b33e34 --- /dev/null +++ b/snow/consensus/snowman/bootstrapper/sampler_test.go @@ -0,0 +1,75 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapper + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/set" + + safemath "github.com/ava-labs/avalanchego/utils/math" +) + +func TestSample(t *testing.T) { + tests := []struct { + name string + elements map[ids.NodeID]uint64 + size int + expectedSampled set.Set[ids.NodeID] + expectedErr error + }{ + { + name: "sample everything", + elements: map[ids.NodeID]uint64{ + nodeID0: 1, + nodeID1: 1, + }, + size: 2, + expectedSampled: set.Of(nodeID0, nodeID1), + expectedErr: nil, + }, + { + name: "limit sample due to too few elements", + elements: map[ids.NodeID]uint64{ + nodeID0: 1, + }, + size: 2, + expectedSampled: set.Of(nodeID0), + expectedErr: nil, + }, + { + name: "limit sample", + elements: map[ids.NodeID]uint64{ + nodeID0: math.MaxUint64 - 1, + nodeID1: 1, + }, + size: 1, + expectedSampled: set.Of(nodeID0), + expectedErr: nil, + }, + { + name: "overflow", + elements: map[ids.NodeID]uint64{ + nodeID0: math.MaxUint64, + nodeID1: 1, + }, + size: 1, + expectedSampled: nil, + expectedErr: safemath.ErrOverflow, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + sampled, err := Sample(test.elements, test.size) + require.ErrorIs(err, test.expectedErr) + require.Equal(test.expectedSampled, sampled) + }) + } +} diff --git a/snow/engine/common/bootstrapper.go b/snow/engine/common/bootstrapper.go index 4f90ca6d9a6b..8ea5320ed15d 100644 --- a/snow/engine/common/bootstrapper.go +++ b/snow/engine/common/bootstrapper.go @@ -122,16 +122,22 @@ func (b *bootstrapper) Startup(ctx context.Context) error { nodeWeights[nodeID] = beacon.Weight } - bootstrapper, err := smbootstrapper.New( + frontierNodes, err := smbootstrapper.Sample(nodeWeights, b.SampleK) + if err != nil { + return err + } + + b.Ctx.Log.Debug("sampled nodes to seed bootstrapping frontier", + zap.Reflect("sampledNodes", frontierNodes), + zap.Int("numNodes", len(nodeWeights)), + ) + + b.bootstrapper = smbootstrapper.New( b.Ctx.Log, + frontierNodes, nodeWeights, - b.Config.SampleK, MaxOutstandingBroadcastRequests, ) - if err != nil { - return err - } - b.bootstrapper = bootstrapper b.bootstrapAttempts++ if accepted, finalized := b.bootstrapper.GetAccepted(ctx); finalized { From 0ba0384564d07e5f1094a44bf85bc3b73a37fe3e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 14 Nov 2023 16:16:01 -0500 Subject: [PATCH 06/16] nit --- snow/consensus/snowman/bootstrapper/majority.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/snow/consensus/snowman/bootstrapper/majority.go b/snow/consensus/snowman/bootstrapper/majority.go index 771889e36c40..7ff14cb6829c 100644 --- a/snow/consensus/snowman/bootstrapper/majority.go +++ b/snow/consensus/snowman/bootstrapper/majority.go @@ -29,8 +29,10 @@ type majority struct { pendingSendAccepted set.Set[ids.NodeID] outstandingAccepted set.Set[ids.NodeID] - receivedAccepted map[ids.ID]uint64 - accepted []ids.ID + // receivedAccepted maps the blockID to the total sum of weight that has + // reported that block as accepted. + receivedAccepted map[ids.ID]uint64 + accepted []ids.ID } func New( From c88726559fa2a9a6ee6a7824c58a0391852b4d10 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 14 Nov 2023 16:23:46 -0500 Subject: [PATCH 07/16] document sample --- snow/consensus/snowman/bootstrapper/sampler.go | 11 +++++++---- snow/consensus/snowman/bootstrapper/sampler_test.go | 12 ++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/snow/consensus/snowman/bootstrapper/sampler.go b/snow/consensus/snowman/bootstrapper/sampler.go index a02543c152a3..b19f9ec23b66 100644 --- a/snow/consensus/snowman/bootstrapper/sampler.go +++ b/snow/consensus/snowman/bootstrapper/sampler.go @@ -9,7 +9,10 @@ import ( "github.com/ava-labs/avalanchego/utils/set" ) -func Sample[T comparable](elements map[T]uint64, size int) (set.Set[T], error) { +// Sample keys from [elements] uniformly by weight without replacement. The +// returned set will have size less than or equal to [maxSize]. This function +// will error if the sum of all weights overflows. +func Sample[T comparable](elements map[T]uint64, maxSize int) (set.Set[T], error) { var ( keys = make([]T, len(elements)) weights = make([]uint64, len(elements)) @@ -32,13 +35,13 @@ func Sample[T comparable](elements map[T]uint64, size int) (set.Set[T], error) { return nil, err } - size = int(math.Min(uint64(size), totalWeight)) - indicies, err := sampler.Sample(size) + maxSize = int(math.Min(uint64(maxSize), totalWeight)) + indicies, err := sampler.Sample(maxSize) if err != nil { return nil, err } - sampledElements := set.NewSet[T](size) + sampledElements := set.NewSet[T](maxSize) for _, index := range indicies { sampledElements.Add(keys[index]) } diff --git a/snow/consensus/snowman/bootstrapper/sampler_test.go b/snow/consensus/snowman/bootstrapper/sampler_test.go index 34cb41b33e34..1b9e366decc7 100644 --- a/snow/consensus/snowman/bootstrapper/sampler_test.go +++ b/snow/consensus/snowman/bootstrapper/sampler_test.go @@ -19,7 +19,7 @@ func TestSample(t *testing.T) { tests := []struct { name string elements map[ids.NodeID]uint64 - size int + maxSize int expectedSampled set.Set[ids.NodeID] expectedErr error }{ @@ -29,7 +29,7 @@ func TestSample(t *testing.T) { nodeID0: 1, nodeID1: 1, }, - size: 2, + maxSize: 2, expectedSampled: set.Of(nodeID0, nodeID1), expectedErr: nil, }, @@ -38,7 +38,7 @@ func TestSample(t *testing.T) { elements: map[ids.NodeID]uint64{ nodeID0: 1, }, - size: 2, + maxSize: 2, expectedSampled: set.Of(nodeID0), expectedErr: nil, }, @@ -48,7 +48,7 @@ func TestSample(t *testing.T) { nodeID0: math.MaxUint64 - 1, nodeID1: 1, }, - size: 1, + maxSize: 1, expectedSampled: set.Of(nodeID0), expectedErr: nil, }, @@ -58,7 +58,7 @@ func TestSample(t *testing.T) { nodeID0: math.MaxUint64, nodeID1: 1, }, - size: 1, + maxSize: 1, expectedSampled: nil, expectedErr: safemath.ErrOverflow, }, @@ -67,7 +67,7 @@ func TestSample(t *testing.T) { t.Run(test.name, func(t *testing.T) { require := require.New(t) - sampled, err := Sample(test.elements, test.size) + sampled, err := Sample(test.elements, test.maxSize) require.ErrorIs(err, test.expectedErr) require.Equal(test.expectedSampled, sampled) }) From fd41c5a034fea70e5f90d28516b38302ec74677e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 14 Nov 2023 16:40:03 -0500 Subject: [PATCH 08/16] Remove subtle invariant --- snow/engine/common/bootstrapper.go | 36 +++++++++--------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/snow/engine/common/bootstrapper.go b/snow/engine/common/bootstrapper.go index 8ea5320ed15d..42c391904337 100644 --- a/snow/engine/common/bootstrapper.go +++ b/snow/engine/common/bootstrapper.go @@ -66,7 +66,7 @@ func (b *bootstrapper) AcceptedFrontier(ctx context.Context, nodeID ids.NodeID, } b.bootstrapper.RecordAcceptedFrontier(ctx, nodeID, containerID) - return b.receivedAcceptedFrontier(ctx) + return b.sendMessagesOrFinish(ctx) } func (b *bootstrapper) GetAcceptedFrontierFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32) error { @@ -80,7 +80,7 @@ func (b *bootstrapper) GetAcceptedFrontierFailed(ctx context.Context, nodeID ids } b.bootstrapper.RecordAcceptedFrontier(ctx, nodeID) - return b.receivedAcceptedFrontier(ctx) + return b.sendMessagesOrFinish(ctx) } func (b *bootstrapper) Accepted(ctx context.Context, nodeID ids.NodeID, requestID uint32, containerIDs []ids.ID) error { @@ -96,7 +96,7 @@ func (b *bootstrapper) Accepted(ctx context.Context, nodeID ids.NodeID, requestI if err := b.bootstrapper.RecordAccepted(ctx, nodeID, containerIDs); err != nil { return err } - return b.receivedAccepted(ctx) + return b.sendMessagesOrFinish(ctx) } func (b *bootstrapper) GetAcceptedFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32) error { @@ -112,7 +112,7 @@ func (b *bootstrapper) GetAcceptedFailed(ctx context.Context, nodeID ids.NodeID, if err := b.bootstrapper.RecordAccepted(ctx, nodeID, nil); err != nil { return err } - return b.receivedAccepted(ctx) + return b.sendMessagesOrFinish(ctx) } func (b *bootstrapper) Startup(ctx context.Context) error { @@ -148,7 +148,7 @@ func (b *bootstrapper) Startup(ctx context.Context) error { } b.Config.SharedCfg.RequestID++ - return b.receivedAcceptedFrontier(ctx) + return b.sendMessagesOrFinish(ctx) } func (b *bootstrapper) Restart(ctx context.Context, reset bool) error { @@ -170,36 +170,20 @@ func (b *bootstrapper) Restart(ctx context.Context, reset bool) error { return b.Startup(ctx) } -func (b *bootstrapper) receivedAcceptedFrontier(ctx context.Context) error { - peers := b.bootstrapper.GetAcceptedFrontiersToSend(ctx) - if peers.Len() > 0 { +func (b *bootstrapper) sendMessagesOrFinish(ctx context.Context) error { + if peers := b.bootstrapper.GetAcceptedFrontiersToSend(ctx); peers.Len() > 0 { b.Sender.SendGetAcceptedFrontier(ctx, peers, b.Config.SharedCfg.RequestID) return nil } - // We haven't finalized the accepted frontier, so we should wait for the - // outstanding requests. - _, finalized := b.bootstrapper.GetAcceptedFrontier(ctx) - if !finalized { - return nil - } - - b.Config.SharedCfg.RequestID++ - return b.receivedAccepted(ctx) -} - -func (b *bootstrapper) receivedAccepted(ctx context.Context) error { potentialAccepted, finalized := b.bootstrapper.GetAcceptedFrontier(ctx) if !finalized { - // We should never receive an accepted message when the frontier isn't - // finalized, as we should have never sent any GetAccepted messages - // before the frontier is finalized. - b.Ctx.Log.Error("bootstrapping frontier unexpectedly not finalized") + // We haven't finalized the accepted frontier, so we should wait for the + // outstanding requests. return nil } - peers := b.bootstrapper.GetAcceptedToSend(ctx) - if peers.Len() > 0 { + if peers := b.bootstrapper.GetAcceptedToSend(ctx); peers.Len() > 0 { b.Sender.SendGetAccepted(ctx, peers, b.Config.SharedCfg.RequestID, potentialAccepted) return nil } From a629c00c52e0e37e85ebb97e902964c068e5be57 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 14 Nov 2023 16:41:38 -0500 Subject: [PATCH 09/16] Add comment --- snow/engine/common/bootstrapper.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/snow/engine/common/bootstrapper.go b/snow/engine/common/bootstrapper.go index 42c391904337..8dbb815d0d75 100644 --- a/snow/engine/common/bootstrapper.go +++ b/snow/engine/common/bootstrapper.go @@ -190,6 +190,8 @@ func (b *bootstrapper) sendMessagesOrFinish(ctx context.Context) error { accepted, finalized := b.bootstrapper.GetAccepted(ctx) if !finalized { + // We haven't finalized the accepted set, so we should wait for the + // outstanding requests. return nil } From 34852b627c4bb10cf1974d050aec75f29a414ff4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 14 Nov 2023 18:21:10 -0500 Subject: [PATCH 10/16] Export majority --- .../snowman/bootstrapper/majority.go | 24 ++++--- .../snowman/bootstrapper/majority_test.go | 70 +++++++++---------- 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/snow/consensus/snowman/bootstrapper/majority.go b/snow/consensus/snowman/bootstrapper/majority.go index 7ff14cb6829c..b76390875970 100644 --- a/snow/consensus/snowman/bootstrapper/majority.go +++ b/snow/consensus/snowman/bootstrapper/majority.go @@ -17,7 +17,9 @@ import ( "github.com/ava-labs/avalanchego/utils/set" ) -type majority struct { +var _ Bootstrapper = (*Majority)(nil) + +type Majority struct { log logging.Logger nodeWeights map[ids.NodeID]uint64 maxOutstanding int @@ -40,8 +42,8 @@ func New( frontierNodes set.Set[ids.NodeID], nodeWeights map[ids.NodeID]uint64, maxOutstanding int, -) Bootstrapper { - return &majority{ +) *Majority { + return &Majority{ log: log, nodeWeights: nodeWeights, maxOutstanding: maxOutstanding, @@ -51,7 +53,7 @@ func New( } } -func (m *majority) GetAcceptedFrontiersToSend(context.Context) set.Set[ids.NodeID] { +func (m *Majority) GetAcceptedFrontiersToSend(context.Context) set.Set[ids.NodeID] { return getPeersToSend( &m.pendingSendAcceptedFrontier, &m.outstandingAcceptedFrontier, @@ -59,7 +61,7 @@ func (m *majority) GetAcceptedFrontiersToSend(context.Context) set.Set[ids.NodeI ) } -func (m *majority) RecordAcceptedFrontier(_ context.Context, nodeID ids.NodeID, blkIDs ...ids.ID) { +func (m *Majority) RecordAcceptedFrontier(_ context.Context, nodeID ids.NodeID, blkIDs ...ids.ID) { if !m.outstandingAcceptedFrontier.Contains(nodeID) { // The chain router should have already dropped unexpected messages. m.log.Error("received unexpected message", @@ -84,11 +86,11 @@ func (m *majority) RecordAcceptedFrontier(_ context.Context, nodeID ids.NodeID, ) } -func (m *majority) GetAcceptedFrontier(context.Context) ([]ids.ID, bool) { +func (m *Majority) GetAcceptedFrontier(context.Context) ([]ids.ID, bool) { return m.receivedAcceptedFrontier, m.finishedFetchingAcceptedFrontiers() } -func (m *majority) GetAcceptedToSend(context.Context) set.Set[ids.NodeID] { +func (m *Majority) GetAcceptedToSend(context.Context) set.Set[ids.NodeID] { if !m.finishedFetchingAcceptedFrontiers() { return nil } @@ -100,7 +102,7 @@ func (m *majority) GetAcceptedToSend(context.Context) set.Set[ids.NodeID] { ) } -func (m *majority) RecordAccepted(_ context.Context, nodeID ids.NodeID, blkIDs []ids.ID) error { +func (m *Majority) RecordAccepted(_ context.Context, nodeID ids.NodeID, blkIDs []ids.ID) error { if !m.outstandingAccepted.Contains(nodeID) { // The chain router should have already dropped unexpected messages. m.log.Error("received unexpected message", @@ -150,16 +152,16 @@ func (m *majority) RecordAccepted(_ context.Context, nodeID ids.NodeID, blkIDs [ return nil } -func (m *majority) GetAccepted(context.Context) ([]ids.ID, bool) { +func (m *Majority) GetAccepted(context.Context) ([]ids.ID, bool) { return m.accepted, m.finishedFetchingAccepted() } -func (m *majority) finishedFetchingAcceptedFrontiers() bool { +func (m *Majority) finishedFetchingAcceptedFrontiers() bool { return m.pendingSendAcceptedFrontier.Len() == 0 && m.outstandingAcceptedFrontier.Len() == 0 } -func (m *majority) finishedFetchingAccepted() bool { +func (m *Majority) finishedFetchingAccepted() bool { return m.pendingSendAccepted.Len() == 0 && m.outstandingAccepted.Len() == 0 } diff --git a/snow/consensus/snowman/bootstrapper/majority_test.go b/snow/consensus/snowman/bootstrapper/majority_test.go index ddc0481ee14f..88532ed3df1c 100644 --- a/snow/consensus/snowman/bootstrapper/majority_test.go +++ b/snow/consensus/snowman/bootstrapper/majority_test.go @@ -37,7 +37,7 @@ func TestNew(t *testing.T) { 2, // maxOutstanding ) - expectedBootstrapper := &majority{ + expectedBootstrapper := &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -60,7 +60,7 @@ func TestMajorityGetAcceptedFrontiersToSend(t *testing.T) { }{ { name: "max outstanding", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -71,7 +71,7 @@ func TestMajorityGetAcceptedFrontiersToSend(t *testing.T) { outstandingAcceptedFrontier: set.Of(nodeID1), receivedAccepted: make(map[ids.ID]uint64), }, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -86,7 +86,7 @@ func TestMajorityGetAcceptedFrontiersToSend(t *testing.T) { }, { name: "send until max outstanding", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -96,7 +96,7 @@ func TestMajorityGetAcceptedFrontiersToSend(t *testing.T) { pendingSendAcceptedFrontier: set.Of(nodeID0, nodeID1), receivedAccepted: make(map[ids.ID]uint64), }, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -111,7 +111,7 @@ func TestMajorityGetAcceptedFrontiersToSend(t *testing.T) { }, { name: "send until no more to send", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -120,7 +120,7 @@ func TestMajorityGetAcceptedFrontiersToSend(t *testing.T) { pendingSendAcceptedFrontier: set.Of(nodeID0), receivedAccepted: make(map[ids.ID]uint64), }, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -154,7 +154,7 @@ func TestMajorityRecordAcceptedFrontier(t *testing.T) { }{ { name: "unexpected response", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -167,7 +167,7 @@ func TestMajorityRecordAcceptedFrontier(t *testing.T) { }, nodeID: nodeID0, blkIDs: nil, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -181,7 +181,7 @@ func TestMajorityRecordAcceptedFrontier(t *testing.T) { }, { name: "unfinished after response", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -194,7 +194,7 @@ func TestMajorityRecordAcceptedFrontier(t *testing.T) { }, nodeID: nodeID1, blkIDs: []ids.ID{blkID0}, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -209,7 +209,7 @@ func TestMajorityRecordAcceptedFrontier(t *testing.T) { }, { name: "finished after response", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -221,7 +221,7 @@ func TestMajorityRecordAcceptedFrontier(t *testing.T) { }, nodeID: nodeID1, blkIDs: []ids.ID{blkID0}, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -252,7 +252,7 @@ func TestMajorityGetAcceptedFrontier(t *testing.T) { }{ { name: "not finalized", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -268,7 +268,7 @@ func TestMajorityGetAcceptedFrontier(t *testing.T) { }, { name: "finalized", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -301,7 +301,7 @@ func TestMajorityGetAcceptedToSend(t *testing.T) { }{ { name: "still fetching frontiers", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -311,7 +311,7 @@ func TestMajorityGetAcceptedToSend(t *testing.T) { outstandingAcceptedFrontier: set.Of(nodeID1), receivedAccepted: make(map[ids.ID]uint64), }, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -325,7 +325,7 @@ func TestMajorityGetAcceptedToSend(t *testing.T) { }, { name: "max outstanding", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -336,7 +336,7 @@ func TestMajorityGetAcceptedToSend(t *testing.T) { outstandingAccepted: set.Of(nodeID1), receivedAccepted: make(map[ids.ID]uint64), }, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -351,7 +351,7 @@ func TestMajorityGetAcceptedToSend(t *testing.T) { }, { name: "send until max outstanding", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -361,7 +361,7 @@ func TestMajorityGetAcceptedToSend(t *testing.T) { pendingSendAccepted: set.Of(nodeID0, nodeID1), receivedAccepted: make(map[ids.ID]uint64), }, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -376,7 +376,7 @@ func TestMajorityGetAcceptedToSend(t *testing.T) { }, { name: "send until no more to send", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -385,7 +385,7 @@ func TestMajorityGetAcceptedToSend(t *testing.T) { pendingSendAccepted: set.Of(nodeID0), receivedAccepted: make(map[ids.ID]uint64), }, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -420,7 +420,7 @@ func TestMajorityRecordAccepted(t *testing.T) { }{ { name: "unexpected response", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -433,7 +433,7 @@ func TestMajorityRecordAccepted(t *testing.T) { }, nodeID: nodeID0, blkIDs: nil, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -448,7 +448,7 @@ func TestMajorityRecordAccepted(t *testing.T) { }, { name: "unfinished after response", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 2, @@ -461,7 +461,7 @@ func TestMajorityRecordAccepted(t *testing.T) { }, nodeID: nodeID1, blkIDs: []ids.ID{blkID0}, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 2, @@ -478,7 +478,7 @@ func TestMajorityRecordAccepted(t *testing.T) { }, { name: "overflow during response", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -492,7 +492,7 @@ func TestMajorityRecordAccepted(t *testing.T) { }, nodeID: nodeID1, blkIDs: []ids.ID{blkID0}, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -508,7 +508,7 @@ func TestMajorityRecordAccepted(t *testing.T) { }, { name: "overflow during final response", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -520,7 +520,7 @@ func TestMajorityRecordAccepted(t *testing.T) { }, nodeID: nodeID1, blkIDs: []ids.ID{blkID0}, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -536,7 +536,7 @@ func TestMajorityRecordAccepted(t *testing.T) { }, { name: "finished after response", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -552,7 +552,7 @@ func TestMajorityRecordAccepted(t *testing.T) { }, nodeID: nodeID2, blkIDs: []ids.ID{blkID1}, - expectedState: &majority{ + expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -590,7 +590,7 @@ func TestMajorityGetAccepted(t *testing.T) { }{ { name: "not finalized", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, @@ -606,7 +606,7 @@ func TestMajorityGetAccepted(t *testing.T) { }, { name: "finalized", - bootstrapper: &majority{ + bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, From 881055d527c6afaac998546076d3d2099f020b54 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 14 Nov 2023 19:45:06 -0500 Subject: [PATCH 11/16] Separate bootstrapper into polls (#2313) --- .../snowman/bootstrapper/bootstrapper.go | 53 +- .../snowman/bootstrapper/bootstrapper_test.go | 15 + .../snowman/bootstrapper/majority.go | 155 ++---- .../snowman/bootstrapper/majority_test.go | 494 +++++------------- .../snowman/bootstrapper/minority.go | 76 +++ .../snowman/bootstrapper/minority_test.go | 242 +++++++++ snow/consensus/snowman/bootstrapper/noop.go | 18 +- .../snowman/bootstrapper/noop_test.go | 22 +- .../snowman/bootstrapper/requests.go | 48 ++ snow/engine/common/bootstrapper.go | 36 +- 10 files changed, 589 insertions(+), 570 deletions(-) create mode 100644 snow/consensus/snowman/bootstrapper/bootstrapper_test.go create mode 100644 snow/consensus/snowman/bootstrapper/minority.go create mode 100644 snow/consensus/snowman/bootstrapper/minority_test.go create mode 100644 snow/consensus/snowman/bootstrapper/requests.go diff --git a/snow/consensus/snowman/bootstrapper/bootstrapper.go b/snow/consensus/snowman/bootstrapper/bootstrapper.go index f607da93dafd..c5cd1abce2a2 100644 --- a/snow/consensus/snowman/bootstrapper/bootstrapper.go +++ b/snow/consensus/snowman/bootstrapper/bootstrapper.go @@ -10,47 +10,14 @@ import ( "github.com/ava-labs/avalanchego/utils/set" ) -// Bootstrapper implements the protocol used to determine the initial set of -// accepted blocks to sync to. -// -// The bootstrapping protocol starts by fetching the last accepted block from an -// initial subset of peers. In order for the protocol to find a recently -// accepted block, there must be at least one correct node in this subset of -// peers. If there is not a correct node in the subset of peers, the node will -// not accept an incorrect block. However, the node may be unable to find an -// acceptable block. -// -// Once the last accepted blocks have been fetched from the subset of peers, the -// set of blocks are sent to all peers. Each peer is expected to filter the -// provided blocks and report which of them they consider accepted. If a -// majority of the peers report that a block is accepted, then the node will -// consider that block to be accepted by the network. This assumes that a -// majority of the network is correct. If a majority of the network is -// malicious, the node may accept an incorrect block. -type Bootstrapper interface { - // GetAcceptedFrontiersToSend returns the set of peers whose accepted - // frontier should be requested. It is expected to repeatedly call this - // function along with [RecordAcceptedFrontier] until [GetAcceptedFrontier] - // returns that the frontier is finalized. - GetAcceptedFrontiersToSend(ctx context.Context) (peers set.Set[ids.NodeID]) - // RecordAcceptedFrontier of nodes whose accepted frontiers were requested. - // [blkIDs] is typically either empty, if the request for frontiers failed, - // or a single block. - RecordAcceptedFrontier(ctx context.Context, nodeID ids.NodeID, blkIDs ...ids.ID) - // GetAcceptedFrontier returns the union of all the provided frontiers along - // with a flag to identify that the frontier has finished being calculated. - GetAcceptedFrontier(ctx context.Context) (blkIDs []ids.ID, finalized bool) - - // GetAcceptedFrontiersToSend returns the set of peers who should be - // requested to filter the frontier. It is expected to repeatedly call this - // function along with [RecordAccepted] until [GetAccepted] returns that the - // set is finalized. - GetAcceptedToSend(ctx context.Context) (peers set.Set[ids.NodeID]) - // RecordAccepted blocks of nodes that were requested. [blkIDs] should - // typically be a subset of the frontier. Any returned error should be - // treated as fatal. - RecordAccepted(ctx context.Context, nodeID ids.NodeID, blkIDs []ids.ID) error - // GetAccepted returns a set of accepted blocks along with a flag to - // identify that the set has finished being calculated. - GetAccepted(ctx context.Context) (blkIDs []ids.ID, finalized bool) +type Poll interface { + // GetPeers returns the set of peers whose opinion should be requested. It + // is expected to repeatedly call this function along with [RecordOpinion] + // until [Result] returns finalized. + GetPeers(ctx context.Context) (peers set.Set[ids.NodeID]) + // RecordOpinion of a node whose opinion was requested. + RecordOpinion(ctx context.Context, nodeID ids.NodeID, blkIDs ...ids.ID) error + // Result returns evaluation of all the peer's opinions along with a flag to + // identify that the frontier has finished being calculated. + Result(ctx context.Context) (blkIDs []ids.ID, finalized bool) } diff --git a/snow/consensus/snowman/bootstrapper/bootstrapper_test.go b/snow/consensus/snowman/bootstrapper/bootstrapper_test.go new file mode 100644 index 000000000000..134867ae1822 --- /dev/null +++ b/snow/consensus/snowman/bootstrapper/bootstrapper_test.go @@ -0,0 +1,15 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapper + +import "github.com/ava-labs/avalanchego/ids" + +var ( + nodeID0 = ids.GenerateTestNodeID() + nodeID1 = ids.GenerateTestNodeID() + nodeID2 = ids.GenerateTestNodeID() + + blkID0 = ids.GenerateTestID() + blkID1 = ids.GenerateTestID() +) diff --git a/snow/consensus/snowman/bootstrapper/majority.go b/snow/consensus/snowman/bootstrapper/majority.go index b76390875970..ad7538ac68e3 100644 --- a/snow/consensus/snowman/bootstrapper/majority.go +++ b/snow/consensus/snowman/bootstrapper/majority.go @@ -11,120 +11,72 @@ import ( "golang.org/x/exp/maps" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/message" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/utils/set" ) -var _ Bootstrapper = (*Majority)(nil) - +var _ Poll = (*Majority)(nil) + +// Majority implements the bootstrapping poll to filter the initial set of +// potentially accaptable blocks into a set of accepted blocks to sync to. +// +// Once the last accepted blocks have been fetched from the initial set of +// peers, the set of blocks are sent to all peers. Each peer is expected to +// filter the provided blocks and report which of them they consider accepted. +// If a majority of the peers report that a block is accepted, then the node +// will consider that block to be accepted by the network. This assumes that a +// majority of the network is correct. If a majority of the network is +// malicious, the node may accept an incorrect block. type Majority struct { - log logging.Logger - nodeWeights map[ids.NodeID]uint64 - maxOutstanding int - - pendingSendAcceptedFrontier set.Set[ids.NodeID] - outstandingAcceptedFrontier set.Set[ids.NodeID] - receivedAcceptedFrontierSet set.Set[ids.ID] - receivedAcceptedFrontier []ids.ID - - pendingSendAccepted set.Set[ids.NodeID] - outstandingAccepted set.Set[ids.NodeID] - // receivedAccepted maps the blockID to the total sum of weight that has - // reported that block as accepted. - receivedAccepted map[ids.ID]uint64 - accepted []ids.ID + requests + + log logging.Logger + nodeWeights map[ids.NodeID]uint64 + + // received maps the blockID to the total sum of weight that has reported + // that block as accepted. + received map[ids.ID]uint64 + accepted []ids.ID } -func New( +func NewMajority( log logging.Logger, - frontierNodes set.Set[ids.NodeID], nodeWeights map[ids.NodeID]uint64, maxOutstanding int, ) *Majority { return &Majority{ - log: log, - nodeWeights: nodeWeights, - maxOutstanding: maxOutstanding, - pendingSendAcceptedFrontier: frontierNodes, - pendingSendAccepted: set.Of(maps.Keys(nodeWeights)...), - receivedAccepted: make(map[ids.ID]uint64), + requests: requests{ + maxOutstanding: maxOutstanding, + pendingSend: set.Of(maps.Keys(nodeWeights)...), + }, + log: log, + nodeWeights: nodeWeights, + received: make(map[ids.ID]uint64), } } -func (m *Majority) GetAcceptedFrontiersToSend(context.Context) set.Set[ids.NodeID] { - return getPeersToSend( - &m.pendingSendAcceptedFrontier, - &m.outstandingAcceptedFrontier, - m.maxOutstanding, - ) -} - -func (m *Majority) RecordAcceptedFrontier(_ context.Context, nodeID ids.NodeID, blkIDs ...ids.ID) { - if !m.outstandingAcceptedFrontier.Contains(nodeID) { +func (m *Majority) RecordOpinion(_ context.Context, nodeID ids.NodeID, blkIDs ...ids.ID) error { + if !m.recordResponse(nodeID) { // The chain router should have already dropped unexpected messages. - m.log.Error("received unexpected message", - zap.Stringer("messageOp", message.AcceptedFrontierOp), + m.log.Error("received unexpected opinion", + zap.String("pollType", "majority"), zap.Stringer("nodeID", nodeID), zap.Stringers("blkIDs", blkIDs), ) - return - } - - m.outstandingAcceptedFrontier.Remove(nodeID) - m.receivedAcceptedFrontierSet.Add(blkIDs...) - - if !m.finishedFetchingAcceptedFrontiers() { - return - } - - m.receivedAcceptedFrontier = m.receivedAcceptedFrontierSet.List() - - m.log.Debug("finalized bootstrapping frontier", - zap.Stringers("frontier", m.receivedAcceptedFrontier), - ) -} - -func (m *Majority) GetAcceptedFrontier(context.Context) ([]ids.ID, bool) { - return m.receivedAcceptedFrontier, m.finishedFetchingAcceptedFrontiers() -} - -func (m *Majority) GetAcceptedToSend(context.Context) set.Set[ids.NodeID] { - if !m.finishedFetchingAcceptedFrontiers() { return nil } - return getPeersToSend( - &m.pendingSendAccepted, - &m.outstandingAccepted, - m.maxOutstanding, - ) -} - -func (m *Majority) RecordAccepted(_ context.Context, nodeID ids.NodeID, blkIDs []ids.ID) error { - if !m.outstandingAccepted.Contains(nodeID) { - // The chain router should have already dropped unexpected messages. - m.log.Error("received unexpected message", - zap.Stringer("messageOp", message.AcceptedOp), - zap.Stringer("nodeID", nodeID), - zap.Stringers("blkIDs", blkIDs), - ) - return nil - } - - m.outstandingAccepted.Remove(nodeID) - weight := m.nodeWeights[nodeID] for _, blkID := range blkIDs { - newWeight, err := math.Add64(m.receivedAccepted[blkID], weight) + newWeight, err := math.Add64(m.received[blkID], weight) if err != nil { return err } - m.receivedAccepted[blkID] = newWeight + m.received[blkID] = newWeight } - if !m.finishedFetchingAccepted() { + if !m.finished() { return nil } @@ -140,7 +92,7 @@ func (m *Majority) RecordAccepted(_ context.Context, nodeID ids.NodeID, blkIDs [ } requiredWeight := totalWeight/2 + 1 - for blkID, weight := range m.receivedAccepted { + for blkID, weight := range m.received { if weight >= requiredWeight { m.accepted = append(m.accepted, blkID) } @@ -152,35 +104,6 @@ func (m *Majority) RecordAccepted(_ context.Context, nodeID ids.NodeID, blkIDs [ return nil } -func (m *Majority) GetAccepted(context.Context) ([]ids.ID, bool) { - return m.accepted, m.finishedFetchingAccepted() -} - -func (m *Majority) finishedFetchingAcceptedFrontiers() bool { - return m.pendingSendAcceptedFrontier.Len() == 0 && - m.outstandingAcceptedFrontier.Len() == 0 -} - -func (m *Majority) finishedFetchingAccepted() bool { - return m.pendingSendAccepted.Len() == 0 && - m.outstandingAccepted.Len() == 0 -} - -func getPeersToSend(pendingSend, outstanding *set.Set[ids.NodeID], maxOutstanding int) set.Set[ids.NodeID] { - numPending := outstanding.Len() - if numPending >= maxOutstanding { - return nil - } - - numToSend := math.Min( - maxOutstanding-numPending, - pendingSend.Len(), - ) - nodeIDs := set.NewSet[ids.NodeID](numToSend) - for i := 0; i < numToSend; i++ { - nodeID, _ := pendingSend.Pop() - nodeIDs.Add(nodeID) - } - outstanding.Union(nodeIDs) - return nodeIDs +func (m *Majority) Result(context.Context) ([]ids.ID, bool) { + return m.accepted, m.finished() } diff --git a/snow/consensus/snowman/bootstrapper/majority_test.go b/snow/consensus/snowman/bootstrapper/majority_test.go index 88532ed3df1c..7399eb711d1d 100644 --- a/snow/consensus/snowman/bootstrapper/majority_test.go +++ b/snow/consensus/snowman/bootstrapper/majority_test.go @@ -17,19 +17,9 @@ import ( safemath "github.com/ava-labs/avalanchego/utils/math" ) -var ( - nodeID0 = ids.GenerateTestNodeID() - nodeID1 = ids.GenerateTestNodeID() - nodeID2 = ids.GenerateTestNodeID() - - blkID0 = ids.GenerateTestID() - blkID1 = ids.GenerateTestID() -) - -func TestNew(t *testing.T) { - bootstrapper := New( +func TestNewMajority(t *testing.T) { + majority := NewMajority( logging.NoLog{}, // log - set.Of(nodeID0), // frontierNodes map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: 1, @@ -37,363 +27,111 @@ func TestNew(t *testing.T) { 2, // maxOutstanding ) - expectedBootstrapper := &Majority{ + expectedMajority := &Majority{ + requests: requests{ + maxOutstanding: 2, + pendingSend: set.Of(nodeID0, nodeID1), + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: 1, }, - maxOutstanding: 2, - pendingSendAcceptedFrontier: set.Of(nodeID0), - pendingSendAccepted: set.Of(nodeID0, nodeID1), - receivedAccepted: make(map[ids.ID]uint64), + received: make(map[ids.ID]uint64), } - require.Equal(t, expectedBootstrapper, bootstrapper) + require.Equal(t, expectedMajority, majority) } -func TestMajorityGetAcceptedFrontiersToSend(t *testing.T) { +func TestMajorityGetPeers(t *testing.T) { tests := []struct { name string - bootstrapper Bootstrapper - expectedState Bootstrapper + majority Poll + expectedState Poll expectedPeers set.Set[ids.NodeID] }{ { name: "max outstanding", - bootstrapper: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, + majority: &Majority{ + requests: requests{ + maxOutstanding: 1, + pendingSend: set.Of(nodeID0), + outstanding: set.Of(nodeID1), }, - maxOutstanding: 1, - pendingSendAcceptedFrontier: set.Of(nodeID0), - outstandingAcceptedFrontier: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), - }, - expectedState: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: 1, }, - maxOutstanding: 1, - pendingSendAcceptedFrontier: set.Of(nodeID0), - outstandingAcceptedFrontier: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), - }, - expectedPeers: nil, - }, - { - name: "send until max outstanding", - bootstrapper: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, - }, - maxOutstanding: 2, - pendingSendAcceptedFrontier: set.Of(nodeID0, nodeID1), - receivedAccepted: make(map[ids.ID]uint64), + received: make(map[ids.ID]uint64), }, expectedState: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, + requests: requests{ + maxOutstanding: 1, + pendingSend: set.Of(nodeID0), + outstanding: set.Of(nodeID1), }, - maxOutstanding: 2, - pendingSendAcceptedFrontier: set.Set[ids.NodeID]{}, - outstandingAcceptedFrontier: set.Of(nodeID0, nodeID1), - receivedAccepted: make(map[ids.ID]uint64), - }, - expectedPeers: set.Of(nodeID0, nodeID1), - }, - { - name: "send until no more to send", - bootstrapper: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - }, - maxOutstanding: 2, - pendingSendAcceptedFrontier: set.Of(nodeID0), - receivedAccepted: make(map[ids.ID]uint64), - }, - expectedState: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - }, - maxOutstanding: 2, - pendingSendAcceptedFrontier: set.Set[ids.NodeID]{}, - outstandingAcceptedFrontier: set.Of(nodeID0), - receivedAccepted: make(map[ids.ID]uint64), - }, - expectedPeers: set.Of(nodeID0), - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - require := require.New(t) - - peers := test.bootstrapper.GetAcceptedFrontiersToSend(context.Background()) - require.Equal(test.expectedState, test.bootstrapper) - require.Equal(test.expectedPeers, peers) - }) - } -} - -func TestMajorityRecordAcceptedFrontier(t *testing.T) { - tests := []struct { - name string - bootstrapper Bootstrapper - nodeID ids.NodeID - blkIDs []ids.ID - expectedState Bootstrapper - }{ - { - name: "unexpected response", - bootstrapper: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, - }, - maxOutstanding: 1, - pendingSendAcceptedFrontier: set.Of(nodeID0), - outstandingAcceptedFrontier: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), - }, - nodeID: nodeID0, - blkIDs: nil, - expectedState: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, - }, - maxOutstanding: 1, - pendingSendAcceptedFrontier: set.Of(nodeID0), - outstandingAcceptedFrontier: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), - }, - }, - { - name: "unfinished after response", - bootstrapper: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, - }, - maxOutstanding: 1, - pendingSendAcceptedFrontier: set.Of(nodeID0), - outstandingAcceptedFrontier: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), - }, - nodeID: nodeID1, - blkIDs: []ids.ID{blkID0}, - expectedState: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, - }, - maxOutstanding: 1, - pendingSendAcceptedFrontier: set.Of(nodeID0), - outstandingAcceptedFrontier: set.Set[ids.NodeID]{}, - receivedAcceptedFrontierSet: set.Of(blkID0), - receivedAccepted: make(map[ids.ID]uint64), - }, - }, - { - name: "finished after response", - bootstrapper: &Majority{ log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: 1, }, - maxOutstanding: 1, - outstandingAcceptedFrontier: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), - }, - nodeID: nodeID1, - blkIDs: []ids.ID{blkID0}, - expectedState: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, - }, - maxOutstanding: 1, - outstandingAcceptedFrontier: set.Set[ids.NodeID]{}, - receivedAcceptedFrontierSet: set.Of(blkID0), - receivedAcceptedFrontier: []ids.ID{blkID0}, - receivedAccepted: make(map[ids.ID]uint64), - }, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - test.bootstrapper.RecordAcceptedFrontier(context.Background(), test.nodeID, test.blkIDs...) - require.Equal(t, test.expectedState, test.bootstrapper) - }) - } -} - -func TestMajorityGetAcceptedFrontier(t *testing.T) { - tests := []struct { - name string - bootstrapper Bootstrapper - expectedAcceptedFrontier []ids.ID - expectedFinalized bool - }{ - { - name: "not finalized", - bootstrapper: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, - }, - maxOutstanding: 1, - outstandingAcceptedFrontier: set.Of(nodeID1), - receivedAcceptedFrontier: nil, - receivedAccepted: make(map[ids.ID]uint64), - }, - expectedAcceptedFrontier: nil, - expectedFinalized: false, - }, - { - name: "finalized", - bootstrapper: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, - }, - maxOutstanding: 1, - receivedAcceptedFrontier: []ids.ID{blkID0}, - }, - expectedAcceptedFrontier: []ids.ID{blkID0}, - expectedFinalized: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - require := require.New(t) - - acceptedFrontier, finalized := test.bootstrapper.GetAcceptedFrontier(context.Background()) - require.Equal(test.expectedAcceptedFrontier, acceptedFrontier) - require.Equal(test.expectedFinalized, finalized) - }) - } -} - -func TestMajorityGetAcceptedToSend(t *testing.T) { - tests := []struct { - name string - bootstrapper Bootstrapper - expectedState Bootstrapper - expectedPeers set.Set[ids.NodeID] - }{ - { - name: "still fetching frontiers", - bootstrapper: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, - }, - maxOutstanding: 1, - outstandingAcceptedFrontier: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), - }, - expectedState: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, - }, - maxOutstanding: 1, - outstandingAcceptedFrontier: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), - }, - expectedPeers: nil, - }, - { - name: "max outstanding", - bootstrapper: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, - }, - maxOutstanding: 1, - pendingSendAccepted: set.Of(nodeID0), - outstandingAccepted: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), - }, - expectedState: &Majority{ - log: logging.NoLog{}, - nodeWeights: map[ids.NodeID]uint64{ - nodeID0: 1, - nodeID1: 1, - }, - maxOutstanding: 1, - pendingSendAccepted: set.Of(nodeID0), - outstandingAccepted: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), + received: make(map[ids.ID]uint64), }, expectedPeers: nil, }, { name: "send until max outstanding", - bootstrapper: &Majority{ + majority: &Majority{ + requests: requests{ + maxOutstanding: 2, + pendingSend: set.Of(nodeID0, nodeID1), + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: 1, }, - maxOutstanding: 2, - pendingSendAccepted: set.Of(nodeID0, nodeID1), - receivedAccepted: make(map[ids.ID]uint64), + received: make(map[ids.ID]uint64), }, expectedState: &Majority{ + requests: requests{ + maxOutstanding: 2, + pendingSend: set.Set[ids.NodeID]{}, + outstanding: set.Of(nodeID0, nodeID1), + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: 1, }, - maxOutstanding: 2, - pendingSendAccepted: set.Set[ids.NodeID]{}, - outstandingAccepted: set.Of(nodeID0, nodeID1), - receivedAccepted: make(map[ids.ID]uint64), + received: make(map[ids.ID]uint64), }, expectedPeers: set.Of(nodeID0, nodeID1), }, { name: "send until no more to send", - bootstrapper: &Majority{ + majority: &Majority{ + requests: requests{ + maxOutstanding: 2, + pendingSend: set.Of(nodeID0), + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, }, - maxOutstanding: 2, - pendingSendAccepted: set.Of(nodeID0), - receivedAccepted: make(map[ids.ID]uint64), + received: make(map[ids.ID]uint64), }, expectedState: &Majority{ + requests: requests{ + maxOutstanding: 2, + pendingSend: set.Set[ids.NodeID]{}, + outstanding: set.Of(nodeID0), + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, }, - maxOutstanding: 2, - pendingSendAccepted: set.Set[ids.NodeID]{}, - outstandingAccepted: set.Of(nodeID0), - receivedAccepted: make(map[ids.ID]uint64), + received: make(map[ids.ID]uint64), }, expectedPeers: set.Of(nodeID0), }, @@ -402,75 +140,83 @@ func TestMajorityGetAcceptedToSend(t *testing.T) { t.Run(test.name, func(t *testing.T) { require := require.New(t) - peers := test.bootstrapper.GetAcceptedToSend(context.Background()) - require.Equal(test.expectedState, test.bootstrapper) + peers := test.majority.GetPeers(context.Background()) + require.Equal(test.expectedState, test.majority) require.Equal(test.expectedPeers, peers) }) } } -func TestMajorityRecordAccepted(t *testing.T) { +func TestMajorityRecordOpinion(t *testing.T) { tests := []struct { name string - bootstrapper Bootstrapper + majority Poll nodeID ids.NodeID blkIDs []ids.ID - expectedState Bootstrapper + expectedState Poll expectedErr error }{ { name: "unexpected response", - bootstrapper: &Majority{ + majority: &Majority{ + requests: requests{ + maxOutstanding: 1, + pendingSend: set.Of(nodeID0), + outstanding: set.Of(nodeID1), + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: 1, }, - maxOutstanding: 1, - pendingSendAccepted: set.Of(nodeID0), - outstandingAccepted: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), + received: make(map[ids.ID]uint64), }, nodeID: nodeID0, blkIDs: nil, expectedState: &Majority{ + requests: requests{ + maxOutstanding: 1, + pendingSend: set.Of(nodeID0), + outstanding: set.Of(nodeID1), + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: 1, }, - maxOutstanding: 1, - pendingSendAccepted: set.Of(nodeID0), - outstandingAccepted: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), + received: make(map[ids.ID]uint64), }, expectedErr: nil, }, { name: "unfinished after response", - bootstrapper: &Majority{ + majority: &Majority{ + requests: requests{ + maxOutstanding: 1, + pendingSend: set.Of(nodeID0), + outstanding: set.Of(nodeID1), + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 2, nodeID1: 3, }, - maxOutstanding: 1, - pendingSendAccepted: set.Of(nodeID0), - outstandingAccepted: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), + received: make(map[ids.ID]uint64), }, nodeID: nodeID1, blkIDs: []ids.ID{blkID0}, expectedState: &Majority{ + requests: requests{ + maxOutstanding: 1, + pendingSend: set.Of(nodeID0), + outstanding: set.Set[ids.NodeID]{}, + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 2, nodeID1: 3, }, - maxOutstanding: 1, - pendingSendAccepted: set.Of(nodeID0), - outstandingAccepted: set.Set[ids.NodeID]{}, - receivedAccepted: map[ids.ID]uint64{ + received: map[ids.ID]uint64{ blkID0: 3, }, }, @@ -478,29 +224,33 @@ func TestMajorityRecordAccepted(t *testing.T) { }, { name: "overflow during response", - bootstrapper: &Majority{ + majority: &Majority{ + requests: requests{ + maxOutstanding: 1, + outstanding: set.Of(nodeID1), + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: math.MaxUint64, }, - maxOutstanding: 1, - outstandingAccepted: set.Of(nodeID1), - receivedAccepted: map[ids.ID]uint64{ + received: map[ids.ID]uint64{ blkID0: 1, }, }, nodeID: nodeID1, blkIDs: []ids.ID{blkID0}, expectedState: &Majority{ + requests: requests{ + maxOutstanding: 1, + outstanding: set.Set[ids.NodeID]{}, + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: math.MaxUint64, }, - maxOutstanding: 1, - outstandingAccepted: set.Set[ids.NodeID]{}, - receivedAccepted: map[ids.ID]uint64{ + received: map[ids.ID]uint64{ blkID0: 1, }, }, @@ -508,27 +258,31 @@ func TestMajorityRecordAccepted(t *testing.T) { }, { name: "overflow during final response", - bootstrapper: &Majority{ + majority: &Majority{ + requests: requests{ + maxOutstanding: 1, + outstanding: set.Of(nodeID1), + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: math.MaxUint64, }, - maxOutstanding: 1, - outstandingAccepted: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), + received: make(map[ids.ID]uint64), }, nodeID: nodeID1, blkIDs: []ids.ID{blkID0}, expectedState: &Majority{ + requests: requests{ + maxOutstanding: 1, + outstanding: set.Set[ids.NodeID]{}, + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: math.MaxUint64, }, - maxOutstanding: 1, - outstandingAccepted: set.Set[ids.NodeID]{}, - receivedAccepted: map[ids.ID]uint64{ + received: map[ids.ID]uint64{ blkID0: math.MaxUint64, }, }, @@ -536,16 +290,18 @@ func TestMajorityRecordAccepted(t *testing.T) { }, { name: "finished after response", - bootstrapper: &Majority{ + majority: &Majority{ + requests: requests{ + maxOutstanding: 1, + outstanding: set.Of(nodeID2), + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: 1, nodeID2: 1, }, - maxOutstanding: 1, - outstandingAccepted: set.Of(nodeID2), - receivedAccepted: map[ids.ID]uint64{ + received: map[ids.ID]uint64{ blkID0: 1, blkID1: 1, }, @@ -553,15 +309,17 @@ func TestMajorityRecordAccepted(t *testing.T) { nodeID: nodeID2, blkIDs: []ids.ID{blkID1}, expectedState: &Majority{ + requests: requests{ + maxOutstanding: 1, + outstanding: set.Set[ids.NodeID]{}, + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: 1, nodeID2: 1, }, - maxOutstanding: 1, - outstandingAccepted: set.Set[ids.NodeID]{}, - receivedAccepted: map[ids.ID]uint64{ + received: map[ids.ID]uint64{ blkID0: 1, blkID1: 2, }, @@ -574,46 +332,50 @@ func TestMajorityRecordAccepted(t *testing.T) { t.Run(test.name, func(t *testing.T) { require := require.New(t) - err := test.bootstrapper.RecordAccepted(context.Background(), test.nodeID, test.blkIDs) - require.Equal(test.expectedState, test.bootstrapper) + err := test.majority.RecordOpinion(context.Background(), test.nodeID, test.blkIDs...) + require.Equal(test.expectedState, test.majority) require.ErrorIs(err, test.expectedErr) }) } } -func TestMajorityGetAccepted(t *testing.T) { +func TestMajorityResult(t *testing.T) { tests := []struct { name string - bootstrapper Bootstrapper + majority Poll expectedAccepted []ids.ID expectedFinalized bool }{ { name: "not finalized", - bootstrapper: &Majority{ + majority: &Majority{ + requests: requests{ + maxOutstanding: 1, + outstanding: set.Of(nodeID1), + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: 1, }, - maxOutstanding: 1, - outstandingAccepted: set.Of(nodeID1), - receivedAccepted: make(map[ids.ID]uint64), - accepted: nil, + received: make(map[ids.ID]uint64), + accepted: nil, }, expectedAccepted: nil, expectedFinalized: false, }, { name: "finalized", - bootstrapper: &Majority{ + majority: &Majority{ + requests: requests{ + maxOutstanding: 1, + }, log: logging.NoLog{}, nodeWeights: map[ids.NodeID]uint64{ nodeID0: 1, nodeID1: 1, }, - maxOutstanding: 1, - receivedAccepted: map[ids.ID]uint64{ + received: map[ids.ID]uint64{ blkID0: 2, }, accepted: []ids.ID{blkID0}, @@ -626,7 +388,7 @@ func TestMajorityGetAccepted(t *testing.T) { t.Run(test.name, func(t *testing.T) { require := require.New(t) - accepted, finalized := test.bootstrapper.GetAccepted(context.Background()) + accepted, finalized := test.majority.Result(context.Background()) require.Equal(test.expectedAccepted, accepted) require.Equal(test.expectedFinalized, finalized) }) diff --git a/snow/consensus/snowman/bootstrapper/minority.go b/snow/consensus/snowman/bootstrapper/minority.go new file mode 100644 index 000000000000..daa2157cacaf --- /dev/null +++ b/snow/consensus/snowman/bootstrapper/minority.go @@ -0,0 +1,76 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapper + +import ( + "context" + + "go.uber.org/zap" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" +) + +var _ Poll = (*Minority)(nil) + +// Minority implements the bootstrapping poll to determine the initial set of +// potentially accaptable blocks. +// +// This poll fetches the last accepted block from an initial set of peers. In +// order for the protocol to find a recently accepted block, there must be at +// least one correct node in this set of peers. If there is not a correct node +// in the set of peers, the node will not accept an incorrect block. However, +// the node may be unable to find an acceptable block. +type Minority struct { + requests + + log logging.Logger + + receivedSet set.Set[ids.ID] + received []ids.ID +} + +func NewMinority( + log logging.Logger, + frontierNodes set.Set[ids.NodeID], + maxOutstanding int, +) *Minority { + return &Minority{ + requests: requests{ + maxOutstanding: maxOutstanding, + pendingSend: frontierNodes, + }, + log: log, + } +} + +func (m *Minority) RecordOpinion(_ context.Context, nodeID ids.NodeID, blkIDs ...ids.ID) error { + if !m.recordResponse(nodeID) { + // The chain router should have already dropped unexpected messages. + m.log.Error("received unexpected opinion", + zap.String("pollType", "minority"), + zap.Stringer("nodeID", nodeID), + zap.Stringers("blkIDs", blkIDs), + ) + return nil + } + + m.receivedSet.Add(blkIDs...) + + if !m.finished() { + return nil + } + + m.received = m.receivedSet.List() + + m.log.Debug("finalized bootstrapping frontier", + zap.Stringers("frontier", m.received), + ) + return nil +} + +func (m *Minority) Result(context.Context) ([]ids.ID, bool) { + return m.received, m.finished() +} diff --git a/snow/consensus/snowman/bootstrapper/minority_test.go b/snow/consensus/snowman/bootstrapper/minority_test.go new file mode 100644 index 000000000000..921d48b0548a --- /dev/null +++ b/snow/consensus/snowman/bootstrapper/minority_test.go @@ -0,0 +1,242 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapper + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" +) + +func TestNewMinority(t *testing.T) { + minority := NewMinority( + logging.NoLog{}, // log + set.Of(nodeID0), // frontierNodes + 2, // maxOutstanding + ) + + expectedMinority := &Minority{ + requests: requests{ + maxOutstanding: 2, + pendingSend: set.Of(nodeID0), + }, + log: logging.NoLog{}, + } + require.Equal(t, expectedMinority, minority) +} + +func TestMinorityGetPeers(t *testing.T) { + tests := []struct { + name string + minority Poll + expectedState Poll + expectedPeers set.Set[ids.NodeID] + }{ + { + name: "max outstanding", + minority: &Minority{ + requests: requests{ + maxOutstanding: 1, + pendingSend: set.Of(nodeID0), + outstanding: set.Of(nodeID1), + }, + log: logging.NoLog{}, + }, + expectedState: &Minority{ + requests: requests{ + maxOutstanding: 1, + pendingSend: set.Of(nodeID0), + outstanding: set.Of(nodeID1), + }, + log: logging.NoLog{}, + }, + expectedPeers: nil, + }, + { + name: "send until max outstanding", + minority: &Minority{ + requests: requests{ + maxOutstanding: 2, + pendingSend: set.Of(nodeID0, nodeID1), + }, + log: logging.NoLog{}, + }, + expectedState: &Minority{ + requests: requests{ + maxOutstanding: 2, + pendingSend: set.Set[ids.NodeID]{}, + outstanding: set.Of(nodeID0, nodeID1), + }, + log: logging.NoLog{}, + }, + expectedPeers: set.Of(nodeID0, nodeID1), + }, + { + name: "send until no more to send", + minority: &Minority{ + requests: requests{ + maxOutstanding: 2, + pendingSend: set.Of(nodeID0), + }, + log: logging.NoLog{}, + }, + expectedState: &Minority{ + requests: requests{ + maxOutstanding: 2, + pendingSend: set.Set[ids.NodeID]{}, + outstanding: set.Of(nodeID0), + }, + log: logging.NoLog{}, + }, + expectedPeers: set.Of(nodeID0), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + peers := test.minority.GetPeers(context.Background()) + require.Equal(test.expectedState, test.minority) + require.Equal(test.expectedPeers, peers) + }) + } +} + +func TestMinorityRecordOpinion(t *testing.T) { + tests := []struct { + name string + minority Poll + nodeID ids.NodeID + blkIDs []ids.ID + expectedState Poll + expectedErr error + }{ + { + name: "unexpected response", + minority: &Minority{ + requests: requests{ + maxOutstanding: 1, + pendingSend: set.Of(nodeID0), + outstanding: set.Of(nodeID1), + }, + log: logging.NoLog{}, + }, + nodeID: nodeID0, + blkIDs: nil, + expectedState: &Minority{ + requests: requests{ + maxOutstanding: 1, + pendingSend: set.Of(nodeID0), + outstanding: set.Of(nodeID1), + }, + log: logging.NoLog{}, + }, + expectedErr: nil, + }, + { + name: "unfinished after response", + minority: &Minority{ + requests: requests{ + maxOutstanding: 1, + pendingSend: set.Of(nodeID0), + outstanding: set.Of(nodeID1), + }, + log: logging.NoLog{}, + }, + nodeID: nodeID1, + blkIDs: []ids.ID{blkID0}, + expectedState: &Minority{ + requests: requests{ + maxOutstanding: 1, + pendingSend: set.Of(nodeID0), + outstanding: set.Set[ids.NodeID]{}, + }, + log: logging.NoLog{}, + receivedSet: set.Of(blkID0), + }, + expectedErr: nil, + }, + { + name: "finished after response", + minority: &Minority{ + requests: requests{ + maxOutstanding: 1, + outstanding: set.Of(nodeID2), + }, + log: logging.NoLog{}, + }, + nodeID: nodeID2, + blkIDs: []ids.ID{blkID1}, + expectedState: &Minority{ + requests: requests{ + maxOutstanding: 1, + outstanding: set.Set[ids.NodeID]{}, + }, + log: logging.NoLog{}, + receivedSet: set.Of(blkID1), + received: []ids.ID{blkID1}, + }, + expectedErr: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + err := test.minority.RecordOpinion(context.Background(), test.nodeID, test.blkIDs...) + require.Equal(test.expectedState, test.minority) + require.ErrorIs(err, test.expectedErr) + }) + } +} + +func TestMinorityResult(t *testing.T) { + tests := []struct { + name string + minority Poll + expectedAccepted []ids.ID + expectedFinalized bool + }{ + { + name: "not finalized", + minority: &Minority{ + requests: requests{ + maxOutstanding: 1, + outstanding: set.Of(nodeID1), + }, + log: logging.NoLog{}, + received: nil, + }, + expectedAccepted: nil, + expectedFinalized: false, + }, + { + name: "finalized", + minority: &Minority{ + requests: requests{ + maxOutstanding: 1, + }, + log: logging.NoLog{}, + receivedSet: set.Of(blkID0), + received: []ids.ID{blkID0}, + }, + expectedAccepted: []ids.ID{blkID0}, + expectedFinalized: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + accepted, finalized := test.minority.Result(context.Background()) + require.Equal(test.expectedAccepted, accepted) + require.Equal(test.expectedFinalized, finalized) + }) + } +} diff --git a/snow/consensus/snowman/bootstrapper/noop.go b/snow/consensus/snowman/bootstrapper/noop.go index b5ff2d410ba2..6ed5fc141e52 100644 --- a/snow/consensus/snowman/bootstrapper/noop.go +++ b/snow/consensus/snowman/bootstrapper/noop.go @@ -10,28 +10,18 @@ import ( "github.com/ava-labs/avalanchego/utils/set" ) -var Noop Bootstrapper = noop{} +var Noop Poll = noop{} type noop struct{} -func (noop) GetAcceptedFrontiersToSend(context.Context) set.Set[ids.NodeID] { +func (noop) GetPeers(context.Context) set.Set[ids.NodeID] { return nil } -func (noop) RecordAcceptedFrontier(context.Context, ids.NodeID, ...ids.ID) {} - -func (noop) GetAcceptedFrontier(context.Context) ([]ids.ID, bool) { - return nil, false -} - -func (noop) GetAcceptedToSend(context.Context) set.Set[ids.NodeID] { - return nil -} - -func (noop) RecordAccepted(context.Context, ids.NodeID, []ids.ID) error { +func (noop) RecordOpinion(context.Context, ids.NodeID, ...ids.ID) error { return nil } -func (noop) GetAccepted(context.Context) ([]ids.ID, bool) { +func (noop) Result(context.Context) ([]ids.ID, bool) { return nil, false } diff --git a/snow/consensus/snowman/bootstrapper/noop_test.go b/snow/consensus/snowman/bootstrapper/noop_test.go index 96f312465c63..f4d299c38adf 100644 --- a/snow/consensus/snowman/bootstrapper/noop_test.go +++ b/snow/consensus/snowman/bootstrapper/noop_test.go @@ -8,30 +8,16 @@ import ( "testing" "github.com/stretchr/testify/require" - - "github.com/ava-labs/avalanchego/ids" ) func TestNoop(t *testing.T) { - var ( - require = require.New(t) - ctx = context.Background() - nodeID = ids.GenerateTestNodeID() - ) - - require.Empty(Noop.GetAcceptedFrontiersToSend(ctx)) - - Noop.RecordAcceptedFrontier(ctx, nodeID) - - blkIDs, finalized := Noop.GetAcceptedFrontier(ctx) - require.Empty(blkIDs) - require.False(finalized) + require := require.New(t) - require.Empty(Noop.GetAcceptedToSend(ctx)) + require.Empty(Noop.GetPeers(context.Background())) - require.NoError(Noop.RecordAccepted(ctx, nodeID, nil)) + require.NoError(Noop.RecordOpinion(context.Background(), nodeID0)) - blkIDs, finalized = Noop.GetAccepted(ctx) + blkIDs, finalized := Noop.Result(context.Background()) require.Empty(blkIDs) require.False(finalized) } diff --git a/snow/consensus/snowman/bootstrapper/requests.go b/snow/consensus/snowman/bootstrapper/requests.go new file mode 100644 index 000000000000..28fc25ce1643 --- /dev/null +++ b/snow/consensus/snowman/bootstrapper/requests.go @@ -0,0 +1,48 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrapper + +import ( + "context" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/utils/set" +) + +type requests struct { + maxOutstanding int + + pendingSend set.Set[ids.NodeID] + outstanding set.Set[ids.NodeID] +} + +func (r *requests) GetPeers(context.Context) set.Set[ids.NodeID] { + numPending := r.outstanding.Len() + if numPending >= r.maxOutstanding { + return nil + } + + numToSend := math.Min( + r.maxOutstanding-numPending, + r.pendingSend.Len(), + ) + nodeIDs := set.NewSet[ids.NodeID](numToSend) + for i := 0; i < numToSend; i++ { + nodeID, _ := r.pendingSend.Pop() + nodeIDs.Add(nodeID) + } + r.outstanding.Union(nodeIDs) + return nodeIDs +} + +func (r *requests) recordResponse(nodeID ids.NodeID) bool { + wasOutstanding := r.outstanding.Contains(nodeID) + r.outstanding.Remove(nodeID) + return wasOutstanding +} + +func (r *requests) finished() bool { + return r.pendingSend.Len() == 0 && r.outstanding.Len() == 0 +} diff --git a/snow/engine/common/bootstrapper.go b/snow/engine/common/bootstrapper.go index 8dbb815d0d75..62cd1341483f 100644 --- a/snow/engine/common/bootstrapper.go +++ b/snow/engine/common/bootstrapper.go @@ -42,7 +42,8 @@ type bootstrapper struct { Config Halter - bootstrapper smbootstrapper.Bootstrapper + minority smbootstrapper.Poll + majority smbootstrapper.Poll // number of times the bootstrap has been attempted bootstrapAttempts int @@ -50,8 +51,9 @@ type bootstrapper struct { func NewCommonBootstrapper(config Config) Bootstrapper { return &bootstrapper{ - Config: config, - bootstrapper: smbootstrapper.Noop, + Config: config, + minority: smbootstrapper.Noop, + majority: smbootstrapper.Noop, } } @@ -65,7 +67,9 @@ func (b *bootstrapper) AcceptedFrontier(ctx context.Context, nodeID ids.NodeID, return nil } - b.bootstrapper.RecordAcceptedFrontier(ctx, nodeID, containerID) + if err := b.minority.RecordOpinion(ctx, nodeID, containerID); err != nil { + return err + } return b.sendMessagesOrFinish(ctx) } @@ -79,7 +83,9 @@ func (b *bootstrapper) GetAcceptedFrontierFailed(ctx context.Context, nodeID ids return nil } - b.bootstrapper.RecordAcceptedFrontier(ctx, nodeID) + if err := b.minority.RecordOpinion(ctx, nodeID); err != nil { + return err + } return b.sendMessagesOrFinish(ctx) } @@ -93,7 +99,7 @@ func (b *bootstrapper) Accepted(ctx context.Context, nodeID ids.NodeID, requestI return nil } - if err := b.bootstrapper.RecordAccepted(ctx, nodeID, containerIDs); err != nil { + if err := b.majority.RecordOpinion(ctx, nodeID, containerIDs...); err != nil { return err } return b.sendMessagesOrFinish(ctx) @@ -109,7 +115,7 @@ func (b *bootstrapper) GetAcceptedFailed(ctx context.Context, nodeID ids.NodeID, return nil } - if err := b.bootstrapper.RecordAccepted(ctx, nodeID, nil); err != nil { + if err := b.majority.RecordOpinion(ctx, nodeID); err != nil { return err } return b.sendMessagesOrFinish(ctx) @@ -132,15 +138,19 @@ func (b *bootstrapper) Startup(ctx context.Context) error { zap.Int("numNodes", len(nodeWeights)), ) - b.bootstrapper = smbootstrapper.New( + b.minority = smbootstrapper.NewMinority( b.Ctx.Log, frontierNodes, + MaxOutstandingBroadcastRequests, + ) + b.majority = smbootstrapper.NewMajority( + b.Ctx.Log, nodeWeights, MaxOutstandingBroadcastRequests, ) b.bootstrapAttempts++ - if accepted, finalized := b.bootstrapper.GetAccepted(ctx); finalized { + if accepted, finalized := b.majority.Result(ctx); finalized { b.Ctx.Log.Info("bootstrapping skipped", zap.String("reason", "no provided bootstraps"), ) @@ -171,24 +181,24 @@ func (b *bootstrapper) Restart(ctx context.Context, reset bool) error { } func (b *bootstrapper) sendMessagesOrFinish(ctx context.Context) error { - if peers := b.bootstrapper.GetAcceptedFrontiersToSend(ctx); peers.Len() > 0 { + if peers := b.minority.GetPeers(ctx); peers.Len() > 0 { b.Sender.SendGetAcceptedFrontier(ctx, peers, b.Config.SharedCfg.RequestID) return nil } - potentialAccepted, finalized := b.bootstrapper.GetAcceptedFrontier(ctx) + potentialAccepted, finalized := b.minority.Result(ctx) if !finalized { // We haven't finalized the accepted frontier, so we should wait for the // outstanding requests. return nil } - if peers := b.bootstrapper.GetAcceptedToSend(ctx); peers.Len() > 0 { + if peers := b.majority.GetPeers(ctx); peers.Len() > 0 { b.Sender.SendGetAccepted(ctx, peers, b.Config.SharedCfg.RequestID, potentialAccepted) return nil } - accepted, finalized := b.bootstrapper.GetAccepted(ctx) + accepted, finalized := b.majority.Result(ctx) if !finalized { // We haven't finalized the accepted set, so we should wait for the // outstanding requests. From 5b9ae56f5eb14899c03741fae7711bd0a9d60764 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 14 Nov 2023 19:46:01 -0500 Subject: [PATCH 12/16] nit --- snow/consensus/snowman/bootstrapper/{bootstrapper.go => poll.go} | 0 .../snowman/bootstrapper/{bootstrapper_test.go => poll_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename snow/consensus/snowman/bootstrapper/{bootstrapper.go => poll.go} (100%) rename snow/consensus/snowman/bootstrapper/{bootstrapper_test.go => poll_test.go} (100%) diff --git a/snow/consensus/snowman/bootstrapper/bootstrapper.go b/snow/consensus/snowman/bootstrapper/poll.go similarity index 100% rename from snow/consensus/snowman/bootstrapper/bootstrapper.go rename to snow/consensus/snowman/bootstrapper/poll.go diff --git a/snow/consensus/snowman/bootstrapper/bootstrapper_test.go b/snow/consensus/snowman/bootstrapper/poll_test.go similarity index 100% rename from snow/consensus/snowman/bootstrapper/bootstrapper_test.go rename to snow/consensus/snowman/bootstrapper/poll_test.go From 38bff639c4c5a0188d41085de63d8ae5ae28d4d1 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 14 Nov 2023 19:47:50 -0500 Subject: [PATCH 13/16] nit --- snow/consensus/snowman/bootstrapper/poll.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snow/consensus/snowman/bootstrapper/poll.go b/snow/consensus/snowman/bootstrapper/poll.go index c5cd1abce2a2..e9d8dbb036fd 100644 --- a/snow/consensus/snowman/bootstrapper/poll.go +++ b/snow/consensus/snowman/bootstrapper/poll.go @@ -17,7 +17,7 @@ type Poll interface { GetPeers(ctx context.Context) (peers set.Set[ids.NodeID]) // RecordOpinion of a node whose opinion was requested. RecordOpinion(ctx context.Context, nodeID ids.NodeID, blkIDs ...ids.ID) error - // Result returns evaluation of all the peer's opinions along with a flag to - // identify that the frontier has finished being calculated. + // Result returns the evaluation of all the peer's opinions along with a + // flag to identify that the result has finished being calculated. Result(ctx context.Context) (blkIDs []ids.ID, finalized bool) } From c8154cbcbbc16c1c83f57e08f936a69792b543d2 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 14 Nov 2023 19:57:13 -0500 Subject: [PATCH 14/16] nit --- snow/engine/common/bootstrapper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snow/engine/common/bootstrapper.go b/snow/engine/common/bootstrapper.go index 62cd1341483f..f9490cce89dc 100644 --- a/snow/engine/common/bootstrapper.go +++ b/snow/engine/common/bootstrapper.go @@ -212,7 +212,7 @@ func (b *bootstrapper) sendMessagesOrFinish(ctx context.Context) error { zap.Int("numBeacons", b.Beacons.Count(b.Ctx.SubnetID)), zap.Int("numBootstrapAttempts", b.bootstrapAttempts), ) - return b.Restart(ctx, false) + return b.Restart(ctx, false /*=reset*/) } if !b.Config.SharedCfg.Restarted { From f990fa125a0fb016a7c068f1ba4a5211dcaa7d6a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 15 Nov 2023 13:02:26 -0500 Subject: [PATCH 15/16] typo --- snow/consensus/snowman/bootstrapper/sampler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snow/consensus/snowman/bootstrapper/sampler.go b/snow/consensus/snowman/bootstrapper/sampler.go index b19f9ec23b66..9511a1e4243f 100644 --- a/snow/consensus/snowman/bootstrapper/sampler.go +++ b/snow/consensus/snowman/bootstrapper/sampler.go @@ -36,13 +36,13 @@ func Sample[T comparable](elements map[T]uint64, maxSize int) (set.Set[T], error } maxSize = int(math.Min(uint64(maxSize), totalWeight)) - indicies, err := sampler.Sample(maxSize) + indices, err := sampler.Sample(maxSize) if err != nil { return nil, err } sampledElements := set.NewSet[T](maxSize) - for _, index := range indicies { + for _, index := range indices { sampledElements.Add(keys[index]) } return sampledElements, nil From a9b96c8983115ff92b4b2f5f0ba7885fa9d627f9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 16 Nov 2023 11:45:37 -0500 Subject: [PATCH 16/16] logging nit --- snow/consensus/snowman/bootstrapper/majority.go | 3 ++- snow/consensus/snowman/bootstrapper/minority.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/snow/consensus/snowman/bootstrapper/majority.go b/snow/consensus/snowman/bootstrapper/majority.go index 175bed89286e..1decb837ef40 100644 --- a/snow/consensus/snowman/bootstrapper/majority.go +++ b/snow/consensus/snowman/bootstrapper/majority.go @@ -98,7 +98,8 @@ func (m *Majority) RecordOpinion(_ context.Context, nodeID ids.NodeID, blkIDs se } } - m.log.Debug("finalized bootstrapping instance", + m.log.Debug("finalized bootstrapping poll", + zap.String("pollType", "majority"), zap.Stringers("accepted", m.accepted), ) return nil diff --git a/snow/consensus/snowman/bootstrapper/minority.go b/snow/consensus/snowman/bootstrapper/minority.go index e4ddbc0f8423..52b45c4407ba 100644 --- a/snow/consensus/snowman/bootstrapper/minority.go +++ b/snow/consensus/snowman/bootstrapper/minority.go @@ -65,7 +65,8 @@ func (m *Minority) RecordOpinion(_ context.Context, nodeID ids.NodeID, blkIDs se m.received = m.receivedSet.List() - m.log.Debug("finalized bootstrapping frontier", + m.log.Debug("finalized bootstrapping poll", + zap.String("pollType", "minority"), zap.Stringers("frontier", m.received), ) return nil