github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/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/onflow/flow-go/consensus/hotstuff" 12 "github.com/onflow/flow-go/consensus/hotstuff/model" 13 "github.com/onflow/flow-go/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.VoteAggregationConsumer 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.VoteAggregationConsumer, 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.VoteAggregationConsumer, 63 verifyingVoteProcessorFactory VerifyingVoteProcessorFactory, 64 ) *VoteCollector { 65 log = log.With(). 66 Str("component", "hotstuff.vote_collector"). 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 invalidVoteErr, ok := model.AsInvalidVoteError(err); ok { 134 m.notifier.OnInvalidVoteDetected(*invalidVoteErr) 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 m.log.Debug().Msgf("duplicated signer %x", vote.SignerID) 142 return nil 143 } 144 return err 145 } 146 147 if currentState != m.Status() { 148 continue 149 } 150 151 m.notifier.OnVoteProcessed(vote) 152 return nil 153 } 154 } 155 156 // Status returns the status of underlying vote processor 157 func (m *VoteCollector) Status() hotstuff.VoteCollectorStatus { 158 return m.atomicLoadProcessor().Status() 159 } 160 161 // View returns view associated with this collector 162 func (m *VoteCollector) View() uint64 { 163 return m.votesCache.View() 164 } 165 166 // ProcessBlock performs validation of block signature and processes block with respected collector. 167 // In case we have received double proposal, we will stop attempting to build a QC for this view, 168 // because we don't want to build on any proposal from an equivocating primary. Note: slashing challenges 169 // for proposal equivocation are triggered by hotstuff.Forks, so we don't have to do anything else here. 170 // 171 // The internal state change is implemented as an atomic compare-and-swap, i.e. 172 // the state transition is only executed if VoteCollector's internal state is 173 // equal to `expectedValue`. The implementation only allows the transitions 174 // 175 // CachingVotes -> VerifyingVotes 176 // CachingVotes -> Invalid 177 // VerifyingVotes -> Invalid 178 func (m *VoteCollector) ProcessBlock(proposal *model.Proposal) error { 179 180 if proposal.Block.View != m.View() { 181 return fmt.Errorf("this VoteCollector requires a proposal for view %d but received block %v with view %d", 182 m.votesCache.View(), proposal.Block.BlockID, proposal.Block.View) 183 } 184 185 for { 186 proc := m.atomicLoadProcessor() 187 188 switch proc.Status() { 189 // first valid block for this view: commence state transition from caching to verifying 190 case hotstuff.VoteCollectorStatusCaching: 191 err := m.caching2Verifying(proposal) 192 if errors.Is(err, ErrDifferentCollectorState) { 193 continue // concurrent state update by other thread => restart our logic 194 } 195 196 if err != nil { 197 return fmt.Errorf("internal error updating VoteProcessor's status from %s to %s for block %v: %w", 198 proc.Status().String(), hotstuff.VoteCollectorStatusVerifying.String(), proposal.Block.BlockID, err) 199 } 200 201 m.log.Info(). 202 Hex("block_id", proposal.Block.BlockID[:]). 203 Msg("vote collector status changed from caching to verifying") 204 205 m.processCachedVotes(proposal.Block) 206 207 // We already received a valid block for this view. Check whether the proposer is 208 // equivocating and terminate vote processing in this case. Note: proposal equivocation 209 // is handled by hotstuff.Forks, so we don't have to do anything else here. 210 case hotstuff.VoteCollectorStatusVerifying: 211 verifyingProc, ok := proc.(hotstuff.VerifyingVoteProcessor) 212 if !ok { 213 return fmt.Errorf("while processing block %v, found that VoteProcessor reports status %s but has an incompatible implementation type %T", 214 proposal.Block.BlockID, proc.Status(), verifyingProc) 215 } 216 if verifyingProc.Block().BlockID != proposal.Block.BlockID { 217 m.terminateVoteProcessing() 218 } 219 220 // Vote processing for this view has already been terminated. Note: proposal equivocation 221 // is handled by hotstuff.Forks, so we don't have anything to do here. 222 case hotstuff.VoteCollectorStatusInvalid: /* no op */ 223 224 default: 225 return fmt.Errorf("while processing block %v, found that VoteProcessor reported unknown status %s", proposal.Block.BlockID, proc.Status()) 226 } 227 228 return nil 229 } 230 } 231 232 // RegisterVoteConsumer registers a VoteConsumer. Upon registration, the collector 233 // feeds all cached votes into the consumer in the order they arrived. 234 // CAUTION, VoteConsumer implementations must be 235 // - NON-BLOCKING and consume the votes without noteworthy delay, and 236 // - CONCURRENCY SAFE 237 func (m *VoteCollector) RegisterVoteConsumer(consumer hotstuff.VoteConsumer) { 238 m.votesCache.RegisterVoteConsumer(consumer) 239 } 240 241 // caching2Verifying ensures that the VoteProcessor is currently in state `VoteCollectorStatusCaching` 242 // and replaces it by a newly-created VerifyingVoteProcessor. 243 // Error returns: 244 // * ErrDifferentCollectorState if the VoteCollector's state is _not_ `CachingVotes` 245 // * all other errors are unexpected and potential symptoms of internal bugs or state corruption (fatal) 246 func (m *VoteCollector) caching2Verifying(proposal *model.Proposal) error { 247 blockID := proposal.Block.BlockID 248 newProc, err := m.createVerifyingProcessor(m.log, proposal) 249 if err != nil { 250 return fmt.Errorf("failed to create VerifyingVoteProcessor for block %v: %w", blockID, err) 251 } 252 newProcWrapper := &atomicValueWrapper{processor: newProc} 253 254 m.Lock() 255 defer m.Unlock() 256 proc := m.atomicLoadProcessor() 257 if proc.Status() != hotstuff.VoteCollectorStatusCaching { 258 return fmt.Errorf("processors's current state is %s: %w", proc.Status().String(), ErrDifferentCollectorState) 259 } 260 m.votesProcessor.Store(newProcWrapper) 261 return nil 262 } 263 264 func (m *VoteCollector) terminateVoteProcessing() { 265 if m.Status() == hotstuff.VoteCollectorStatusInvalid { 266 return 267 } 268 newProcWrapper := &atomicValueWrapper{ 269 processor: NewNoopCollector(hotstuff.VoteCollectorStatusInvalid), 270 } 271 272 m.Lock() 273 defer m.Unlock() 274 m.votesProcessor.Store(newProcWrapper) 275 } 276 277 // processCachedVotes feeds all cached votes into the VoteProcessor 278 func (m *VoteCollector) processCachedVotes(block *model.Block) { 279 cachedVotes := m.votesCache.All() 280 m.log.Info().Msgf("processing %d cached votes", len(cachedVotes)) 281 for _, vote := range cachedVotes { 282 if vote.BlockID != block.BlockID { 283 continue 284 } 285 286 blockVote := vote 287 voteProcessingTask := func() { 288 err := m.processVote(blockVote) 289 if err != nil { 290 m.log.Fatal().Err(err).Msg("internal error processing cached vote") 291 } 292 } 293 m.workers.Submit(voteProcessingTask) 294 } 295 }