github.com/onflow/flow-go@v0.33.17/consensus/hotstuff/votecollector/combined_vote_processor_v2.go (about)

     1  package votecollector
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  
     7  	"github.com/rs/zerolog"
     8  	"go.uber.org/atomic"
     9  
    10  	"github.com/onflow/flow-go/consensus/hotstuff"
    11  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    12  	"github.com/onflow/flow-go/consensus/hotstuff/signature"
    13  	"github.com/onflow/flow-go/consensus/hotstuff/verification"
    14  	"github.com/onflow/flow-go/crypto"
    15  	"github.com/onflow/flow-go/model/flow"
    16  	msig "github.com/onflow/flow-go/module/signature"
    17  )
    18  
    19  /* **************** Base-Factory for CombinedVoteProcessors ***************** */
    20  
    21  // combinedVoteProcessorFactoryBaseV2 is a `votecollector.baseFactory` for creating
    22  // CombinedVoteProcessors, holding all needed dependencies.
    23  // combinedVoteProcessorFactoryBaseV2 is intended to be used for the main consensus.
    24  // CAUTION:
    25  // this base factory only creates the VerifyingVoteProcessor for the given block.
    26  // It does _not_ check the proposer's vote for its own block, i.e. it does _not_
    27  // implement `hotstuff.VoteProcessorFactory`. This base factory should be wrapped
    28  // by `votecollector.VoteProcessorFactory` which adds the logic to verify
    29  // the proposer's vote (decorator pattern).
    30  type combinedVoteProcessorFactoryBaseV2 struct {
    31  	committee   hotstuff.DynamicCommittee
    32  	onQCCreated hotstuff.OnQCCreated
    33  	packer      hotstuff.Packer
    34  }
    35  
    36  // Create creates CombinedVoteProcessorV2 for processing votes for the given block.
    37  // Caller must treat all errors as exceptions
    38  func (f *combinedVoteProcessorFactoryBaseV2) Create(log zerolog.Logger, block *model.Block) (hotstuff.VerifyingVoteProcessor, error) {
    39  	allParticipants, err := f.committee.IdentitiesByBlock(block.BlockID)
    40  	if err != nil {
    41  		return nil, fmt.Errorf("error retrieving consensus participants at block %v: %w", block.BlockID, err)
    42  	}
    43  
    44  	// message that has to be verified against aggregated signature
    45  	msg := verification.MakeVoteMessage(block.View, block.BlockID)
    46  
    47  	// prepare the staking public keys of participants
    48  	stakingKeys := make([]crypto.PublicKey, 0, len(allParticipants))
    49  	for _, participant := range allParticipants {
    50  		stakingKeys = append(stakingKeys, participant.StakingPubKey)
    51  	}
    52  
    53  	stakingSigAggtor, err := signature.NewWeightedSignatureAggregator(allParticipants, stakingKeys, msg, msig.ConsensusVoteTag)
    54  	if err != nil {
    55  		return nil, fmt.Errorf("could not create aggregator for staking signatures at block %v: %w", block.BlockID, err)
    56  	}
    57  
    58  	publicKeyShares := make([]crypto.PublicKey, 0, len(allParticipants))
    59  	dkg, err := f.committee.DKG(block.View)
    60  	if err != nil {
    61  		return nil, fmt.Errorf("could not get DKG info at block %v: %w", block.BlockID, err)
    62  	}
    63  	for _, participant := range allParticipants {
    64  		pk, err := dkg.KeyShare(participant.NodeID)
    65  		if err != nil {
    66  			return nil, fmt.Errorf("could not get random beacon key share for %x at block %v: %w", participant.NodeID, block.BlockID, err)
    67  		}
    68  		publicKeyShares = append(publicKeyShares, pk)
    69  	}
    70  
    71  	threshold := msig.RandomBeaconThreshold(int(dkg.Size()))
    72  	randomBeaconInspector, err := signature.NewRandomBeaconInspector(dkg.GroupKey(), publicKeyShares, threshold, msg)
    73  	if err != nil {
    74  		return nil, fmt.Errorf("could not create random beacon inspector at block %v: %w", block.BlockID, err)
    75  	}
    76  
    77  	rbRector := signature.NewRandomBeaconReconstructor(dkg, randomBeaconInspector)
    78  	minRequiredWeight, err := f.committee.QuorumThresholdForView(block.View)
    79  	if err != nil {
    80  		return nil, fmt.Errorf("could not get weight threshold for view %d: %w", block.View, err)
    81  	}
    82  
    83  	return NewCombinedVoteProcessor(
    84  		log,
    85  		block,
    86  		stakingSigAggtor,
    87  		rbRector,
    88  		f.onQCCreated,
    89  		f.packer,
    90  		minRequiredWeight,
    91  	), nil
    92  }
    93  
    94  /* ****************** CombinedVoteProcessorV2 Implementation ****************** */
    95  
    96  // CombinedVoteProcessorV2 implements the hotstuff.VerifyingVoteProcessor interface.
    97  // It processes votes from the main consensus committee, where participants must
    98  // _always_ provide the staking signature as part of their vote and can _optionally_
    99  // also provide a random beacon signature. Through their staking signature, a
   100  // participant always contributes to HotStuff's progress. Participation in the random
   101  // beacon is optional (but encouraged). This allows nodes that failed the DKG to
   102  // still contribute only to consensus (as fallback).
   103  // CombinedVoteProcessorV2 is Concurrency safe.
   104  type CombinedVoteProcessorV2 struct {
   105  	log               zerolog.Logger
   106  	block             *model.Block
   107  	stakingSigAggtor  hotstuff.WeightedSignatureAggregator
   108  	rbRector          hotstuff.RandomBeaconReconstructor
   109  	onQCCreated       hotstuff.OnQCCreated
   110  	packer            hotstuff.Packer
   111  	minRequiredWeight uint64
   112  	done              atomic.Bool
   113  }
   114  
   115  var _ hotstuff.VerifyingVoteProcessor = (*CombinedVoteProcessorV2)(nil)
   116  
   117  func NewCombinedVoteProcessor(log zerolog.Logger,
   118  	block *model.Block,
   119  	stakingSigAggtor hotstuff.WeightedSignatureAggregator,
   120  	rbRector hotstuff.RandomBeaconReconstructor,
   121  	onQCCreated hotstuff.OnQCCreated,
   122  	packer hotstuff.Packer,
   123  	minRequiredWeight uint64,
   124  ) *CombinedVoteProcessorV2 {
   125  	return &CombinedVoteProcessorV2{
   126  		log:               log.With().Hex("block_id", block.BlockID[:]).Logger(),
   127  		block:             block,
   128  		stakingSigAggtor:  stakingSigAggtor,
   129  		rbRector:          rbRector,
   130  		onQCCreated:       onQCCreated,
   131  		packer:            packer,
   132  		minRequiredWeight: minRequiredWeight,
   133  		done:              *atomic.NewBool(false),
   134  	}
   135  }
   136  
   137  // Block returns block that is part of proposal that we are processing votes for.
   138  func (p *CombinedVoteProcessorV2) Block() *model.Block {
   139  	return p.block
   140  }
   141  
   142  // Status returns status of this vote processor, it's always verifying.
   143  func (p *CombinedVoteProcessorV2) Status() hotstuff.VoteCollectorStatus {
   144  	return hotstuff.VoteCollectorStatusVerifying
   145  }
   146  
   147  // Process performs processing of single vote in concurrent safe way. This function is implemented to be
   148  // called by multiple goroutines at the same time. Supports processing of both staking and random beacon signatures.
   149  // Design of this function is event driven: as soon as we collect enough signatures to create a QC we will immediately do so
   150  // and submit it via callback for further processing.
   151  // Expected error returns during normal operations:
   152  // * VoteForIncompatibleBlockError - submitted vote for incompatible block
   153  // * VoteForIncompatibleViewError - submitted vote for incompatible view
   154  // * model.InvalidVoteError - submitted vote with invalid signature
   155  // * model.DuplicatedSignerError if the signer has been already added
   156  // All other errors should be treated as exceptions.
   157  //
   158  // Impossibility of vote double-counting: Our signature scheme requires _every_ vote to supply a
   159  // staking signature. Therefore, the `stakingSigAggtor` has the set of _all_ signerIDs that have
   160  // provided a valid vote. Hence, the `stakingSigAggtor` guarantees that only a single vote can
   161  // be successfully added for each `signerID`, i.e. double-counting votes is impossible.
   162  func (p *CombinedVoteProcessorV2) Process(vote *model.Vote) error {
   163  	err := EnsureVoteForBlock(vote, p.block)
   164  	if err != nil {
   165  		return fmt.Errorf("received incompatible vote %v: %w", vote.ID(), err)
   166  	}
   167  
   168  	// Vote Processing state machine
   169  	if p.done.Load() {
   170  		return nil
   171  	}
   172  	stakingSig, randomBeaconSig, err := msig.DecodeDoubleSig(vote.SigData)
   173  	if err != nil {
   174  		if errors.Is(err, msig.ErrInvalidSignatureFormat) {
   175  			return model.NewInvalidVoteErrorf(vote, "could not decode signature: %w", err)
   176  		}
   177  		return fmt.Errorf("unexpected error decoding vote %v: %w", vote.ID(), err)
   178  	}
   179  
   180  	// Verify staking sig.
   181  	err = p.stakingSigAggtor.Verify(vote.SignerID, stakingSig)
   182  	if err != nil {
   183  		if model.IsInvalidSignerError(err) {
   184  			return model.NewInvalidVoteErrorf(vote, "vote %x for view %d is not from an authorized consensus participant: %w",
   185  				vote.ID(), vote.View, err)
   186  		}
   187  		if errors.Is(err, model.ErrInvalidSignature) {
   188  			return model.NewInvalidVoteErrorf(vote, "vote %x for view %d has an invalid staking signature: %w",
   189  				vote.ID(), vote.View, err)
   190  		}
   191  		return fmt.Errorf("internal error checking signature validity for vote %v: %w", vote.ID(), err)
   192  	}
   193  
   194  	if p.done.Load() {
   195  		return nil
   196  	}
   197  
   198  	// Verify random beacon sig
   199  	if randomBeaconSig != nil {
   200  		err = p.rbRector.Verify(vote.SignerID, randomBeaconSig)
   201  		if err != nil {
   202  			// InvalidSignerError is possible in case we have consensus participants that are _not_ part of the random beacon committee.
   203  			if model.IsInvalidSignerError(err) {
   204  				return model.NewInvalidVoteErrorf(vote, "vote %x for view %d is not from an authorized random beacon participant: %w",
   205  					vote.ID(), vote.View, err)
   206  			}
   207  			if errors.Is(err, model.ErrInvalidSignature) {
   208  				return model.NewInvalidVoteErrorf(vote, "vote %x for view %d has an invalid random beacon signature: %w",
   209  					vote.ID(), vote.View, err)
   210  			}
   211  			return fmt.Errorf("internal error checking signature validity for vote %v: %w", vote.ID(), err)
   212  		}
   213  	}
   214  
   215  	if p.done.Load() {
   216  		return nil
   217  	}
   218  
   219  	// Add staking sig to aggregator.
   220  	_, err = p.stakingSigAggtor.TrustedAdd(vote.SignerID, stakingSig)
   221  	if err != nil {
   222  		// we don't expect any errors here during normal operation, as we previously checked
   223  		// for duplicated votes from the same signer and verified the signer+signature
   224  		return fmt.Errorf("unexpected exception adding signature from vote %v to staking aggregator: %w", vote.ID(), err)
   225  	}
   226  	// Add random beacon sig to threshold sig reconstructor
   227  	if randomBeaconSig != nil {
   228  		_, err = p.rbRector.TrustedAdd(vote.SignerID, randomBeaconSig)
   229  		if err != nil {
   230  			// we don't expect any errors here during normal operation, as we previously checked
   231  			// for duplicated votes from the same signer and verified the signer+signature
   232  			return fmt.Errorf("unexpected exception adding signature from vote %v to random beacon reconstructor: %w", vote.ID(), err)
   233  		}
   234  	}
   235  
   236  	// checking of conditions for building QC are satisfied
   237  	totalWeight := p.stakingSigAggtor.TotalWeight()
   238  	p.log.Debug().Msgf("processed vote, total weight=(%d), required=(%d)", totalWeight, p.minRequiredWeight)
   239  	if totalWeight < p.minRequiredWeight {
   240  		return nil
   241  	}
   242  	if !p.rbRector.EnoughShares() {
   243  		return nil
   244  	}
   245  
   246  	// At this point, we have enough signatures to build a QC. Another routine
   247  	// might just be at this point. To avoid duplicate work, only one routine can pass:
   248  	if !p.done.CompareAndSwap(false, true) {
   249  		return nil
   250  	}
   251  
   252  	// Our algorithm for checking votes and adding them to the aggregators should
   253  	// guarantee that we are _always_ able to successfully construct a QC when we
   254  	// reach this point. A failure implies that the VoteProcessor's internal state is corrupted.
   255  	qc, err := p.buildQC()
   256  	if err != nil {
   257  		return fmt.Errorf("internal error constructing QC from votes: %w", err)
   258  	}
   259  
   260  	p.log.Info().
   261  		Uint64("view", qc.View).
   262  		Hex("signers", qc.SignerIndices).
   263  		Msg("new QC has been created")
   264  
   265  	p.onQCCreated(qc)
   266  
   267  	return nil
   268  }
   269  
   270  // buildQC performs aggregation and reconstruction of signatures when we have collected enough
   271  // signatures for building a QC. This function is run only once by a single worker.
   272  // Any error should be treated as exception.
   273  func (p *CombinedVoteProcessorV2) buildQC() (*flow.QuorumCertificate, error) {
   274  	stakingSigners, aggregatedStakingSig, err := p.stakingSigAggtor.Aggregate()
   275  	if err != nil {
   276  		return nil, fmt.Errorf("could not aggregate staking signature: %w", err)
   277  	}
   278  	reconstructedBeaconSig, err := p.rbRector.Reconstruct()
   279  	if err != nil {
   280  		return nil, fmt.Errorf("could not reconstruct random beacon group signature: %w", err)
   281  	}
   282  
   283  	blockSigData := buildBlockSignatureDataForV2(stakingSigners, aggregatedStakingSig, reconstructedBeaconSig)
   284  	qc, err := buildQCWithPackerAndSigData(p.packer, p.block, blockSigData)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  
   289  	return qc, nil
   290  }
   291  
   292  // buildBlockSignatureDataForV2 build a block sig data for V2
   293  // It reuses the hotstuff.BlockSignatureData type to create the sig data without filling the RandomBeaconSigners field and
   294  // the AggregatedRandomBeaconSig field, so that the packer can be reused by both V2 and V3 to pack QC's sig data.
   295  func buildBlockSignatureDataForV2(
   296  	stakingSigners []flow.Identifier,
   297  	aggregatedStakingSig []byte,
   298  	reconstructedBeaconSig crypto.Signature,
   299  ) *hotstuff.BlockSignatureData {
   300  	return &hotstuff.BlockSignatureData{
   301  		StakingSigners:               stakingSigners,
   302  		AggregatedStakingSig:         aggregatedStakingSig,
   303  		ReconstructedRandomBeaconSig: reconstructedBeaconSig,
   304  	}
   305  }
   306  
   307  // buildQCWithPackerAndSigData builds the QC with the given packer and blockSigData
   308  func buildQCWithPackerAndSigData(
   309  	packer hotstuff.Packer,
   310  	block *model.Block,
   311  	blockSigData *hotstuff.BlockSignatureData,
   312  ) (*flow.QuorumCertificate, error) {
   313  	signerIndices, sigData, err := packer.Pack(block.View, blockSigData)
   314  
   315  	if err != nil {
   316  		return nil, fmt.Errorf("could not pack the block sig data: %w", err)
   317  	}
   318  
   319  	return &flow.QuorumCertificate{
   320  		View:          block.View,
   321  		BlockID:       block.BlockID,
   322  		SignerIndices: signerIndices,
   323  		SigData:       sigData,
   324  	}, nil
   325  }