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 }