github.com/koko1123/flow-go-1@v0.29.6/consensus/hotstuff/votecollector/statemachine.go (about)

     1  package votecollector
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sync"
     7  
     8  	"github.com/rs/zerolog"
     9  	"go.uber.org/atomic"
    10  
    11  	"github.com/koko1123/flow-go-1/consensus/hotstuff"
    12  	"github.com/koko1123/flow-go-1/consensus/hotstuff/model"
    13  	"github.com/koko1123/flow-go-1/consensus/hotstuff/voteaggregator"
    14  )
    15  
    16  var (
    17  	ErrDifferentCollectorState = errors.New("different state")
    18  )
    19  
    20  // VerifyingVoteProcessorFactory generates hotstuff.VerifyingVoteCollector instances
    21  type VerifyingVoteProcessorFactory = func(log zerolog.Logger, proposal *model.Proposal) (hotstuff.VerifyingVoteProcessor, error)
    22  
    23  // VoteCollector implements a state machine for transition between different states of vote collector
    24  type VoteCollector struct {
    25  	sync.Mutex
    26  	log                      zerolog.Logger
    27  	workers                  hotstuff.Workers
    28  	notifier                 hotstuff.Consumer
    29  	createVerifyingProcessor VerifyingVoteProcessorFactory
    30  
    31  	votesCache     VotesCache
    32  	votesProcessor atomic.Value
    33  }
    34  
    35  var _ hotstuff.VoteCollector = (*VoteCollector)(nil)
    36  
    37  func (m *VoteCollector) atomicLoadProcessor() hotstuff.VoteProcessor {
    38  	return m.votesProcessor.Load().(*atomicValueWrapper).processor
    39  }
    40  
    41  // atomic.Value doesn't allow storing interfaces as atomic values,
    42  // it requires that stored type is always the same, so we need a wrapper that will mitigate this restriction
    43  // https://github.com/golang/go/issues/22550
    44  type atomicValueWrapper struct {
    45  	processor hotstuff.VoteProcessor
    46  }
    47  
    48  func NewStateMachineFactory(
    49  	log zerolog.Logger,
    50  	notifier hotstuff.Consumer,
    51  	verifyingVoteProcessorFactory VerifyingVoteProcessorFactory,
    52  ) voteaggregator.NewCollectorFactoryMethod {
    53  	return func(view uint64, workers hotstuff.Workers) (hotstuff.VoteCollector, error) {
    54  		return NewStateMachine(view, log, workers, notifier, verifyingVoteProcessorFactory), nil
    55  	}
    56  }
    57  
    58  func NewStateMachine(
    59  	view uint64,
    60  	log zerolog.Logger,
    61  	workers hotstuff.Workers,
    62  	notifier hotstuff.Consumer,
    63  	verifyingVoteProcessorFactory VerifyingVoteProcessorFactory,
    64  ) *VoteCollector {
    65  	log = log.With().
    66  		Str("hotstuff", "VoteCollector").
    67  		Uint64("view", view).
    68  		Logger()
    69  	sm := &VoteCollector{
    70  		log:                      log,
    71  		workers:                  workers,
    72  		notifier:                 notifier,
    73  		createVerifyingProcessor: verifyingVoteProcessorFactory,
    74  		votesCache:               *NewVotesCache(view),
    75  	}
    76  
    77  	// without a block, we don't process votes (only cache them)
    78  	sm.votesProcessor.Store(&atomicValueWrapper{
    79  		processor: NewNoopCollector(hotstuff.VoteCollectorStatusCaching),
    80  	})
    81  	return sm
    82  }
    83  
    84  // AddVote adds a vote to current vote collector
    85  // All expected errors are handled via callbacks to notifier.
    86  // Under normal execution only exceptions are propagated to caller.
    87  func (m *VoteCollector) AddVote(vote *model.Vote) error {
    88  	// Cache vote
    89  	err := m.votesCache.AddVote(vote)
    90  	if err != nil {
    91  		if errors.Is(err, RepeatedVoteErr) {
    92  			return nil
    93  		}
    94  		if doubleVoteErr, isDoubleVoteErr := model.AsDoubleVoteError(err); isDoubleVoteErr {
    95  			m.notifier.OnDoubleVotingDetected(doubleVoteErr.FirstVote, doubleVoteErr.ConflictingVote)
    96  			return nil
    97  		}
    98  		return fmt.Errorf("internal error adding vote %v to cache for block %v: %w",
    99  			vote.ID(), vote.BlockID, err)
   100  	}
   101  
   102  	err = m.processVote(vote)
   103  	if err != nil {
   104  		if errors.Is(err, VoteForIncompatibleBlockError) {
   105  			// For honest nodes, there should be only a single proposal per view and all votes should
   106  			// be for this proposal. However, byzantine nodes might deviate from this happy path:
   107  			// * A malicious leader might create multiple (individually valid) conflicting proposals for the
   108  			//   same view. Honest replicas will send correct votes for whatever proposal they see first.
   109  			//   We only accept the first valid block and reject any other conflicting blocks that show up later.
   110  			// * Alternatively, malicious replicas might send votes with the expected view, but for blocks that
   111  			//   don't exist.
   112  			// In either case, receiving votes for the same view but for different block IDs is a symptom
   113  			// of malicious consensus participants.  Hence, we log it here as a warning:
   114  			m.log.Warn().
   115  				Err(err).
   116  				Msg("received vote for incompatible block")
   117  
   118  			return nil
   119  		}
   120  		return fmt.Errorf("internal error processing vote %v for block %v: %w",
   121  			vote.ID(), vote.BlockID, err)
   122  	}
   123  	return nil
   124  }
   125  
   126  // processVote uses compare-and-repeat pattern to process vote with underlying vote processor
   127  func (m *VoteCollector) processVote(vote *model.Vote) error {
   128  	for {
   129  		processor := m.atomicLoadProcessor()
   130  		currentState := processor.Status()
   131  		err := processor.Process(vote)
   132  		if err != nil {
   133  			if model.IsInvalidVoteError(err) {
   134  				m.notifier.OnInvalidVoteDetected(vote)
   135  				return nil
   136  			}
   137  			// ATTENTION: due to how our logic is designed this situation is only possible
   138  			// where we receive the same vote twice, this is not a case of double voting.
   139  			// This scenario is possible if leader submits his vote additionally to the vote in proposal.
   140  			if model.IsDuplicatedSignerError(err) {
   141  				return nil
   142  			}
   143  			return err
   144  		}
   145  
   146  		if currentState != m.Status() {
   147  			continue
   148  		}
   149  
   150  		return nil
   151  	}
   152  }
   153  
   154  // Status returns the status of underlying vote processor
   155  func (m *VoteCollector) Status() hotstuff.VoteCollectorStatus {
   156  	return m.atomicLoadProcessor().Status()
   157  }
   158  
   159  // View returns view associated with this collector
   160  func (m *VoteCollector) View() uint64 {
   161  	return m.votesCache.View()
   162  }
   163  
   164  // ProcessBlock performs validation of block signature and processes block with respected collector.
   165  // In case we have received double proposal, we will stop attempting to build a QC for this view,
   166  // because we don't want to build on any proposal from an equivocating primary. Note: slashing challenges
   167  // for proposal equivocation are triggered by hotstuff.Forks, so we don't have to do anything else here.
   168  //
   169  // The internal state change is implemented as an atomic compare-and-swap, i.e.
   170  // the state transition is only executed if VoteCollector's internal state is
   171  // equal to `expectedValue`. The implementation only allows the transitions
   172  //
   173  //	CachingVotes   -> VerifyingVotes
   174  //	CachingVotes   -> Invalid
   175  //	VerifyingVotes -> Invalid
   176  func (m *VoteCollector) ProcessBlock(proposal *model.Proposal) error {
   177  
   178  	if proposal.Block.View != m.View() {
   179  		return fmt.Errorf("this VoteCollector requires a proposal for view %d but received block %v with view %d",
   180  			m.votesCache.View(), proposal.Block.BlockID, proposal.Block.View)
   181  	}
   182  
   183  	for {
   184  		proc := m.atomicLoadProcessor()
   185  
   186  		switch proc.Status() {
   187  		// first valid block for this view: commence state transition from caching to verifying
   188  		case hotstuff.VoteCollectorStatusCaching:
   189  			err := m.caching2Verifying(proposal)
   190  			if errors.Is(err, ErrDifferentCollectorState) {
   191  				continue // concurrent state update by other thread => restart our logic
   192  			}
   193  
   194  			if err != nil {
   195  				return fmt.Errorf("internal error updating VoteProcessor's status from %s to %s for block %v: %w",
   196  					proc.Status().String(), hotstuff.VoteCollectorStatusVerifying.String(), proposal.Block.BlockID, err)
   197  			}
   198  
   199  			m.log.Info().
   200  				Hex("block_id", proposal.Block.BlockID[:]).
   201  				Msg("vote collector status changed from caching to verifying")
   202  
   203  			m.processCachedVotes(proposal.Block)
   204  
   205  		// We already received a valid block for this view. Check whether the proposer is
   206  		// equivocating and terminate vote processing in this case. Note: proposal equivocation
   207  		// is handled by hotstuff.Forks, so we don't have to do anything else here.
   208  		case hotstuff.VoteCollectorStatusVerifying:
   209  			verifyingProc, ok := proc.(hotstuff.VerifyingVoteProcessor)
   210  			if !ok {
   211  				return fmt.Errorf("while processing block %v, found that VoteProcessor reports status %s but has an incompatible implementation type %T",
   212  					proposal.Block.BlockID, proc.Status(), verifyingProc)
   213  			}
   214  			if verifyingProc.Block().BlockID != proposal.Block.BlockID {
   215  				m.terminateVoteProcessing()
   216  			}
   217  
   218  		// Vote processing for this view has already been terminated. Note: proposal equivocation
   219  		// is handled by hotstuff.Forks, so we don't have anything to do here.
   220  		case hotstuff.VoteCollectorStatusInvalid: /* no op */
   221  
   222  		default:
   223  			return fmt.Errorf("while processing block %v, found that VoteProcessor reported unknown status %s", proposal.Block.BlockID, proc.Status())
   224  		}
   225  
   226  		return nil
   227  	}
   228  }
   229  
   230  // RegisterVoteConsumer registers a VoteConsumer. Upon registration, the collector
   231  // feeds all cached votes into the consumer in the order they arrived.
   232  // CAUTION, VoteConsumer implementations must be
   233  //   - NON-BLOCKING and consume the votes without noteworthy delay, and
   234  //   - CONCURRENCY SAFE
   235  func (m *VoteCollector) RegisterVoteConsumer(consumer hotstuff.VoteConsumer) {
   236  	m.votesCache.RegisterVoteConsumer(consumer)
   237  }
   238  
   239  // caching2Verifying ensures that the VoteProcessor is currently in state `VoteCollectorStatusCaching`
   240  // and replaces it by a newly-created VerifyingVoteProcessor.
   241  // Error returns:
   242  // * ErrDifferentCollectorState if the VoteCollector's state is _not_ `CachingVotes`
   243  // * all other errors are unexpected and potential symptoms of internal bugs or state corruption (fatal)
   244  func (m *VoteCollector) caching2Verifying(proposal *model.Proposal) error {
   245  	blockID := proposal.Block.BlockID
   246  	newProc, err := m.createVerifyingProcessor(m.log, proposal)
   247  	if err != nil {
   248  		return fmt.Errorf("failed to create VerifyingVoteProcessor for block %v: %w", blockID, err)
   249  	}
   250  	newProcWrapper := &atomicValueWrapper{processor: newProc}
   251  
   252  	m.Lock()
   253  	defer m.Unlock()
   254  	proc := m.atomicLoadProcessor()
   255  	if proc.Status() != hotstuff.VoteCollectorStatusCaching {
   256  		return fmt.Errorf("processors's current state is %s: %w", proc.Status().String(), ErrDifferentCollectorState)
   257  	}
   258  	m.votesProcessor.Store(newProcWrapper)
   259  	return nil
   260  }
   261  
   262  func (m *VoteCollector) terminateVoteProcessing() {
   263  	if m.Status() == hotstuff.VoteCollectorStatusInvalid {
   264  		return
   265  	}
   266  	newProcWrapper := &atomicValueWrapper{
   267  		processor: NewNoopCollector(hotstuff.VoteCollectorStatusInvalid),
   268  	}
   269  
   270  	m.Lock()
   271  	defer m.Unlock()
   272  	m.votesProcessor.Store(newProcWrapper)
   273  }
   274  
   275  // processCachedVotes feeds all cached votes into the VoteProcessor
   276  func (m *VoteCollector) processCachedVotes(block *model.Block) {
   277  	for _, vote := range m.votesCache.All() {
   278  		if vote.BlockID != block.BlockID {
   279  			continue
   280  		}
   281  
   282  		blockVote := vote
   283  		voteProcessingTask := func() {
   284  			err := m.processVote(blockVote)
   285  			if err != nil {
   286  				m.log.Fatal().Err(err).Msg("internal error processing cached vote")
   287  			}
   288  		}
   289  		m.workers.Submit(voteProcessingTask)
   290  	}
   291  }