Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Refactor bootstrapper implementation into consensus #2300

Merged
merged 19 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions snow/consensus/snowman/bootstrapper/bootstrapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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"
)

// 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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason these functions include context.Context is to add tracing support. I feel like that can be left to a later PR though... This PR is already quite large.

StephenButtolph marked this conversation as resolved.
Show resolved Hide resolved
// 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
StephenButtolph marked this conversation as resolved.
Show resolved Hide resolved
// 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)

Choose a reason for hiding this comment

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

Why does this return a slice of IDs whereas other methods return a set of IDs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We pass around sets of nodeIDs and slices of IDs. This matches the types expected by the message sender.

}
198 changes: 198 additions & 0 deletions snow/consensus/snowman/bootstrapper/majority.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// 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/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"
)

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])
}
StephenButtolph marked this conversation as resolved.
Show resolved Hide resolved

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) {
// The chain router should have already dropped unexpected messages.
m.log.Error("received unexpected message",
zap.Stringer("messageOp", message.AcceptedFrontierOp),
zap.Stringer("nodeID", nodeID),
zap.Stringers("blkIDs", blkIDs),
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This used to be a DEBUG log because the engine used to need to handle unexpected messages... But these should be filtered by the chain router now.

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
StephenButtolph marked this conversation as resolved.
Show resolved Hide resolved
}

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),
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This used to be a DEBUG log because the engine used to need to handle unexpected messages... But these should be filtered by the chain router now.

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",
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like this log should be done in the caller of Result

Copy link
Contributor Author

@StephenButtolph StephenButtolph Nov 16, 2023

Choose a reason for hiding this comment

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

It isn't particularly easy to move this log out of Minority... I feel like it's cleaner to keep the logs in both Minority and Majority rather than in Minority and the engine

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
}
Loading
Loading