github.com/pokt-network/tendermint@v0.32.11-0.20230426215212-59310158d3e9/docs/architecture/adr-030-consensus-refactor.md (about) 1 # ADR 030: Consensus Refactor 2 3 ## Context 4 5 One of the biggest challenges this project faces is to proof that the 6 implementations of the specifications are correct, much like we strive to 7 formaly verify our alogrithms and protocols we should work towards high 8 confidence about the correctness of our program code. One of those is the core 9 of Tendermint - Consensus - which currently resides in the `consensus` package. 10 Over time there has been high friction making changes to the package due to the 11 algorithm being scattered in a side-effectful container (the current 12 `ConsensusState`). In order to test the algorithm a large object-graph needs to 13 be set up and even than the non-deterministic parts of the container makes will 14 prevent high certainty. Where ideally we have a 1-to-1 representation of the 15 [spec](https://github.com/tendermint/spec), ready and easy to test for domain 16 experts. 17 18 Addresses: 19 20 - [#1495](https://github.com/tendermint/tendermint/issues/1495) 21 - [#1692](https://github.com/tendermint/tendermint/issues/1692) 22 23 ## Decision 24 25 To remedy these issues we plan a gradual, non-invasive refactoring of the 26 `consensus` package. Starting of by isolating the consensus alogrithm into 27 a pure function and a finite state machine to address the most pressuring issue 28 of lack of confidence. Doing so while leaving the rest of the package in tact 29 and have follow-up optional changes to improve the sepration of concerns. 30 31 ### Implementation changes 32 33 The core of Consensus can be modelled as a function with clear defined inputs: 34 35 * `State` - data container for current round, height, etc. 36 * `Event`- significant events in the network 37 38 producing clear outputs; 39 40 * `State` - updated input 41 * `Message` - signal what actions to perform 42 43 ```go 44 type Event int 45 46 const ( 47 EventUnknown Event = iota 48 EventProposal 49 Majority23PrevotesBlock 50 Majority23PrecommitBlock 51 Majority23PrevotesAny 52 Majority23PrecommitAny 53 TimeoutNewRound 54 TimeoutPropose 55 TimeoutPrevotes 56 TimeoutPrecommit 57 ) 58 59 type Message int 60 61 const ( 62 MeesageUnknown Message = iota 63 MessageProposal 64 MessageVotes 65 MessageDecision 66 ) 67 68 type State struct { 69 height uint64 70 round uint64 71 step uint64 72 lockedValue interface{} // TODO: Define proper type. 73 lockedRound interface{} // TODO: Define proper type. 74 validValue interface{} // TODO: Define proper type. 75 validRound interface{} // TODO: Define proper type. 76 // From the original notes: valid(v) 77 valid interface{} // TODO: Define proper type. 78 // From the original notes: proposer(h, r) 79 proposer interface{} // TODO: Define proper type. 80 } 81 82 func Consensus(Event, State) (State, Message) { 83 // Consolidate implementation. 84 } 85 ``` 86 87 Tracking of relevant information to feed `Event` into the function and act on 88 the output is left to the `ConsensusExecutor` (formerly `ConsensusState`). 89 90 Benefits for testing surfacing nicely as testing for a sequence of events 91 against algorithm could be as simple as the following example: 92 93 ``` go 94 func TestConsensusXXX(t *testing.T) { 95 type expected struct { 96 message Message 97 state State 98 } 99 100 // Setup order of events, initial state and expectation. 101 var ( 102 events = []struct { 103 event Event 104 want expected 105 }{ 106 // ... 107 } 108 state = State{ 109 // ... 110 } 111 ) 112 113 for _, e := range events { 114 sate, msg = Consensus(e.event, state) 115 116 // Test message expectation. 117 if msg != e.want.message { 118 t.Fatalf("have %v, want %v", msg, e.want.message) 119 } 120 121 // Test state expectation. 122 if !reflect.DeepEqual(state, e.want.state) { 123 t.Fatalf("have %v, want %v", state, e.want.state) 124 } 125 } 126 } 127 ``` 128 129 130 ## Consensus Executor 131 132 ## Consensus Core 133 134 ```go 135 type Event interface{} 136 137 type EventNewHeight struct { 138 Height int64 139 ValidatorId int 140 } 141 142 type EventNewRound HeightAndRound 143 144 type EventProposal struct { 145 Height int64 146 Round int 147 Timestamp Time 148 BlockID BlockID 149 POLRound int 150 Sender int 151 } 152 153 type Majority23PrevotesBlock struct { 154 Height int64 155 Round int 156 BlockID BlockID 157 } 158 159 type Majority23PrecommitBlock struct { 160 Height int64 161 Round int 162 BlockID BlockID 163 } 164 165 type HeightAndRound struct { 166 Height int64 167 Round int 168 } 169 170 type Majority23PrevotesAny HeightAndRound 171 type Majority23PrecommitAny HeightAndRound 172 type TimeoutPropose HeightAndRound 173 type TimeoutPrevotes HeightAndRound 174 type TimeoutPrecommit HeightAndRound 175 176 177 type Message interface{} 178 179 type MessageProposal struct { 180 Height int64 181 Round int 182 BlockID BlockID 183 POLRound int 184 } 185 186 type VoteType int 187 188 const ( 189 VoteTypeUnknown VoteType = iota 190 Prevote 191 Precommit 192 ) 193 194 195 type MessageVote struct { 196 Height int64 197 Round int 198 BlockID BlockID 199 Type VoteType 200 } 201 202 203 type MessageDecision struct { 204 Height int64 205 Round int 206 BlockID BlockID 207 } 208 209 type TriggerTimeout struct { 210 Height int64 211 Round int 212 Duration Duration 213 } 214 215 216 type RoundStep int 217 218 const ( 219 RoundStepUnknown RoundStep = iota 220 RoundStepPropose 221 RoundStepPrevote 222 RoundStepPrecommit 223 RoundStepCommit 224 ) 225 226 type State struct { 227 Height int64 228 Round int 229 Step RoundStep 230 LockedValue BlockID 231 LockedRound int 232 ValidValue BlockID 233 ValidRound int 234 ValidatorId int 235 ValidatorSetSize int 236 } 237 238 func proposer(height int64, round int) int {} 239 func getValue() BlockID {} 240 241 func Consensus(event Event, state State) (State, Message, TriggerTimeout) { 242 msg = nil 243 timeout = nil 244 switch event := event.(type) { 245 case EventNewHeight: 246 if event.Height > state.Height { 247 state.Height = event.Height 248 state.Round = -1 249 state.Step = RoundStepPropose 250 state.LockedValue = nil 251 state.LockedRound = -1 252 state.ValidValue = nil 253 state.ValidRound = -1 254 state.ValidatorId = event.ValidatorId 255 } 256 return state, msg, timeout 257 258 case EventNewRound: 259 if event.Height == state.Height and event.Round > state.Round { 260 state.Round = eventRound 261 state.Step = RoundStepPropose 262 if proposer(state.Height, state.Round) == state.ValidatorId { 263 proposal = state.ValidValue 264 if proposal == nil { 265 proposal = getValue() 266 } 267 msg = MessageProposal { state.Height, state.Round, proposal, state.ValidRound } 268 } 269 timeout = TriggerTimeout { state.Height, state.Round, timeoutPropose(state.Round) } 270 } 271 return state, msg, timeout 272 273 case EventProposal: 274 if event.Height == state.Height and event.Round == state.Round and 275 event.Sender == proposal(state.Height, state.Round) and state.Step == RoundStepPropose { 276 if event.POLRound >= state.LockedRound or event.BlockID == state.BlockID or state.LockedRound == -1 { 277 msg = MessageVote { state.Height, state.Round, event.BlockID, Prevote } 278 } 279 state.Step = RoundStepPrevote 280 } 281 return state, msg, timeout 282 283 case TimeoutPropose: 284 if event.Height == state.Height and event.Round == state.Round and state.Step == RoundStepPropose { 285 msg = MessageVote { state.Height, state.Round, nil, Prevote } 286 state.Step = RoundStepPrevote 287 } 288 return state, msg, timeout 289 290 case Majority23PrevotesBlock: 291 if event.Height == state.Height and event.Round == state.Round and state.Step >= RoundStepPrevote and event.Round > state.ValidRound { 292 state.ValidRound = event.Round 293 state.ValidValue = event.BlockID 294 if state.Step == RoundStepPrevote { 295 state.LockedRound = event.Round 296 state.LockedValue = event.BlockID 297 msg = MessageVote { state.Height, state.Round, event.BlockID, Precommit } 298 state.Step = RoundStepPrecommit 299 } 300 } 301 return state, msg, timeout 302 303 case Majority23PrevotesAny: 304 if event.Height == state.Height and event.Round == state.Round and state.Step == RoundStepPrevote { 305 timeout = TriggerTimeout { state.Height, state.Round, timeoutPrevote(state.Round) } 306 } 307 return state, msg, timeout 308 309 case TimeoutPrevote: 310 if event.Height == state.Height and event.Round == state.Round and state.Step == RoundStepPrevote { 311 msg = MessageVote { state.Height, state.Round, nil, Precommit } 312 state.Step = RoundStepPrecommit 313 } 314 return state, msg, timeout 315 316 case Majority23PrecommitBlock: 317 if event.Height == state.Height { 318 state.Step = RoundStepCommit 319 state.LockedValue = event.BlockID 320 } 321 return state, msg, timeout 322 323 case Majority23PrecommitAny: 324 if event.Height == state.Height and event.Round == state.Round { 325 timeout = TriggerTimeout { state.Height, state.Round, timeoutPrecommit(state.Round) } 326 } 327 return state, msg, timeout 328 329 case TimeoutPrecommit: 330 if event.Height == state.Height and event.Round == state.Round { 331 state.Round = state.Round + 1 332 } 333 return state, msg, timeout 334 } 335 } 336 337 func ConsensusExecutor() { 338 proposal = nil 339 votes = HeightVoteSet { Height: 1 } 340 state = State { 341 Height: 1 342 Round: 0 343 Step: RoundStepPropose 344 LockedValue: nil 345 LockedRound: -1 346 ValidValue: nil 347 ValidRound: -1 348 } 349 350 event = EventNewHeight {1, id} 351 state, msg, timeout = Consensus(event, state) 352 353 event = EventNewRound {state.Height, 0} 354 state, msg, timeout = Consensus(event, state) 355 356 if msg != nil { 357 send msg 358 } 359 360 if timeout != nil { 361 trigger timeout 362 } 363 364 for { 365 select { 366 case message := <- msgCh: 367 switch msg := message.(type) { 368 case MessageProposal: 369 370 case MessageVote: 371 if msg.Height == state.Height { 372 newVote = votes.AddVote(msg) 373 if newVote { 374 switch msg.Type { 375 case Prevote: 376 prevotes = votes.Prevotes(msg.Round) 377 if prevotes.WeakCertificate() and msg.Round > state.Round { 378 event = EventNewRound { msg.Height, msg.Round } 379 state, msg, timeout = Consensus(event, state) 380 state = handleStateChange(state, msg, timeout) 381 } 382 383 if blockID, ok = prevotes.TwoThirdsMajority(); ok and blockID != nil { 384 if msg.Round == state.Round and hasBlock(blockID) { 385 event = Majority23PrevotesBlock { msg.Height, msg.Round, blockID } 386 state, msg, timeout = Consensus(event, state) 387 state = handleStateChange(state, msg, timeout) 388 } 389 if proposal != nil and proposal.POLRound == msg.Round and hasBlock(blockID) { 390 event = EventProposal { 391 Height: state.Height 392 Round: state.Round 393 BlockID: blockID 394 POLRound: proposal.POLRound 395 Sender: message.Sender 396 } 397 state, msg, timeout = Consensus(event, state) 398 state = handleStateChange(state, msg, timeout) 399 } 400 } 401 402 if prevotes.HasTwoThirdsAny() and msg.Round == state.Round { 403 event = Majority23PrevotesAny { msg.Height, msg.Round, blockID } 404 state, msg, timeout = Consensus(event, state) 405 state = handleStateChange(state, msg, timeout) 406 } 407 408 case Precommit: 409 410 } 411 } 412 } 413 case timeout := <- timeoutCh: 414 415 case block := <- blockCh: 416 417 } 418 } 419 } 420 421 func handleStateChange(state, msg, timeout) State { 422 if state.Step == Commit { 423 state = ExecuteBlock(state.LockedValue) 424 } 425 if msg != nil { 426 send msg 427 } 428 if timeout != nil { 429 trigger timeout 430 } 431 } 432 433 ``` 434 435 ### Implementation roadmap 436 437 * implement proposed implementation 438 * replace currently scattered calls in `ConsensusState` with calls to the new 439 `Consensus` function 440 * rename `ConsensusState` to `ConsensusExecutor` to avoid confusion 441 * propose design for improved separation and clear information flow between 442 `ConsensusExecutor` and `ConsensusReactor` 443 444 ## Status 445 446 Draft. 447 448 ## Consequences 449 450 ### Positive 451 452 - isolated implementation of the algorithm 453 - improved testability - simpler to proof correctness 454 - clearer separation of concerns - easier to reason 455 456 ### Negative 457 458 ### Neutral