github.com/MetalBlockchain/metalgo@v1.11.9/snow/consensus/snowman/bootstrapper/majority.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package bootstrapper
     5  
     6  import (
     7  	"context"
     8  
     9  	"go.uber.org/zap"
    10  	"golang.org/x/exp/maps"
    11  
    12  	"github.com/MetalBlockchain/metalgo/ids"
    13  	"github.com/MetalBlockchain/metalgo/utils/logging"
    14  	"github.com/MetalBlockchain/metalgo/utils/math"
    15  	"github.com/MetalBlockchain/metalgo/utils/set"
    16  )
    17  
    18  var _ Poll = (*Majority)(nil)
    19  
    20  // Majority implements the bootstrapping poll to filter the initial set of
    21  // potentially accaptable blocks into a set of accepted blocks to sync to.
    22  //
    23  // Once the last accepted blocks have been fetched from the initial set of
    24  // peers, the set of blocks are sent to all peers. Each peer is expected to
    25  // filter the provided blocks and report which of them they consider accepted.
    26  // If a majority of the peers report that a block is accepted, then the node
    27  // will consider that block to be accepted by the network. This assumes that a
    28  // majority of the network is correct. If a majority of the network is
    29  // malicious, the node may accept an incorrect block.
    30  type Majority struct {
    31  	requests
    32  
    33  	log         logging.Logger
    34  	nodeWeights map[ids.NodeID]uint64
    35  
    36  	// received maps the blockID to the total sum of weight that has reported
    37  	// that block as accepted.
    38  	received map[ids.ID]uint64
    39  	accepted []ids.ID
    40  }
    41  
    42  func NewMajority(
    43  	log logging.Logger,
    44  	nodeWeights map[ids.NodeID]uint64,
    45  	maxOutstanding int,
    46  ) *Majority {
    47  	return &Majority{
    48  		requests: requests{
    49  			maxOutstanding: maxOutstanding,
    50  			pendingSend:    set.Of(maps.Keys(nodeWeights)...),
    51  		},
    52  		log:         log,
    53  		nodeWeights: nodeWeights,
    54  		received:    make(map[ids.ID]uint64),
    55  	}
    56  }
    57  
    58  func (m *Majority) RecordOpinion(_ context.Context, nodeID ids.NodeID, blkIDs set.Set[ids.ID]) error {
    59  	if !m.recordResponse(nodeID) {
    60  		// The chain router should have already dropped unexpected messages.
    61  		m.log.Error("received unexpected opinion",
    62  			zap.String("pollType", "majority"),
    63  			zap.Stringer("nodeID", nodeID),
    64  			zap.Reflect("blkIDs", blkIDs),
    65  		)
    66  		return nil
    67  	}
    68  
    69  	weight := m.nodeWeights[nodeID]
    70  	for blkID := range blkIDs {
    71  		newWeight, err := math.Add64(m.received[blkID], weight)
    72  		if err != nil {
    73  			return err
    74  		}
    75  		m.received[blkID] = newWeight
    76  	}
    77  
    78  	if !m.finished() {
    79  		return nil
    80  	}
    81  
    82  	var (
    83  		totalWeight uint64
    84  		err         error
    85  	)
    86  	for _, weight := range m.nodeWeights {
    87  		totalWeight, err = math.Add64(totalWeight, weight)
    88  		if err != nil {
    89  			return err
    90  		}
    91  	}
    92  
    93  	requiredWeight := totalWeight/2 + 1
    94  	for blkID, weight := range m.received {
    95  		if weight >= requiredWeight {
    96  			m.accepted = append(m.accepted, blkID)
    97  		}
    98  	}
    99  
   100  	m.log.Debug("finalized bootstrapping poll",
   101  		zap.String("pollType", "majority"),
   102  		zap.Stringers("accepted", m.accepted),
   103  	)
   104  	return nil
   105  }
   106  
   107  func (m *Majority) Result(context.Context) ([]ids.ID, bool) {
   108  	return m.accepted, m.finished()
   109  }