|
|
- # ADR 030: Consensus Refactor
-
- ## Context
-
- One of the biggest challenges this project faces is to proof that the
- implementations of the specifications are correct, much like we strive to
- formaly verify our alogrithms and protocols we should work towards high
- confidence about the correctness of our program code. One of those is the core
- of Tendermint - Consensus - which currently resides in the `consensus` package.
- Over time there has been high friction making changes to the package due to the
- algorithm being scattered in a side-effectful container (the current
- `ConsensusState`). In order to test the algorithm a large object-graph needs to
- be set up and even than the non-deterministic parts of the container makes will
- prevent high certainty. Where ideally we have a 1-to-1 representation of the
- [spec](https://github.com/tendermint/spec), ready and easy to test for domain
- experts.
-
- Addresses:
-
- - [#1495](https://github.com/tendermint/tendermint/issues/1495)
- - [#1692](https://github.com/tendermint/tendermint/issues/1692)
-
- ## Decision
-
- To remedy these issues we plan a gradual, non-invasive refactoring of the
- `consensus` package. Starting of by isolating the consensus alogrithm into
- a pure function and a finite state machine to address the most pressuring issue
- of lack of confidence. Doing so while leaving the rest of the package in tact
- and have follow-up optional changes to improve the sepration of concerns.
-
- ### Implementation changes
-
- The core of Consensus can be modelled as a function with clear defined inputs:
-
- * `State` - data container for current round, height, etc.
- * `Event`- significant events in the network
-
- producing clear outputs;
-
- * `State` - updated input
- * `Message` - signal what actions to perform
-
- ```go
- type Event int
-
- const (
- EventUnknown Event = iota
- EventProposal
- Majority23PrevotesBlock
- Majority23PrecommitBlock
- Majority23PrevotesAny
- Majority23PrecommitAny
- TimeoutNewRound
- TimeoutPropose
- TimeoutPrevotes
- TimeoutPrecommit
- )
-
- type Message int
-
- const (
- MeesageUnknown Message = iota
- MessageProposal
- MessageVotes
- MessageDecision
- )
-
- type State struct {
- height uint64
- round uint64
- step uint64
- lockedValue interface{} // TODO: Define proper type.
- lockedRound interface{} // TODO: Define proper type.
- validValue interface{} // TODO: Define proper type.
- validRound interface{} // TODO: Define proper type.
- // From the original notes: valid(v)
- valid interface{} // TODO: Define proper type.
- // From the original notes: proposer(h, r)
- proposer interface{} // TODO: Define proper type.
- }
-
- func Consensus(Event, State) (State, Message) {
- // Consolidate implementation.
- }
- ```
-
- Tracking of relevant information to feed `Event` into the function and act on
- the output is left to the `ConsensusExecutor` (formerly `ConsensusState`).
-
- Benefits for testing surfacing nicely as testing for a sequence of events
- against algorithm could be as simple as the following example:
-
- ``` go
- func TestConsensusXXX(t *testing.T) {
- type expected struct {
- message Message
- state State
- }
-
- // Setup order of events, initial state and expectation.
- var (
- events = []struct {
- event Event
- want expected
- }{
- // ...
- }
- state = State{
- // ...
- }
- )
-
- for _, e := range events {
- sate, msg = Consensus(e.event, state)
-
- // Test message expectation.
- if msg != e.want.message {
- t.Fatalf("have %v, want %v", msg, e.want.message)
- }
-
- // Test state expectation.
- if !reflect.DeepEqual(state, e.want.state) {
- t.Fatalf("have %v, want %v", state, e.want.state)
- }
- }
- }
- ```
-
-
- ## Consensus Executor
-
- ## Consensus Core
-
- ```go
- type Event interface{}
-
- type EventNewHeight struct {
- Height int64
- ValidatorId int
- }
-
- type EventNewRound HeightAndRound
-
- type EventProposal struct {
- Height int64
- Round int
- Timestamp Time
- BlockID BlockID
- POLRound int
- Sender int
- }
-
- type Majority23PrevotesBlock struct {
- Height int64
- Round int
- BlockID BlockID
- }
-
- type Majority23PrecommitBlock struct {
- Height int64
- Round int
- BlockID BlockID
- }
-
- type HeightAndRound struct {
- Height int64
- Round int
- }
-
- type Majority23PrevotesAny HeightAndRound
- type Majority23PrecommitAny HeightAndRound
- type TimeoutPropose HeightAndRound
- type TimeoutPrevotes HeightAndRound
- type TimeoutPrecommit HeightAndRound
-
-
- type Message interface{}
-
- type MessageProposal struct {
- Height int64
- Round int
- BlockID BlockID
- POLRound int
- }
-
- type VoteType int
-
- const (
- VoteTypeUnknown VoteType = iota
- Prevote
- Precommit
- )
-
-
- type MessageVote struct {
- Height int64
- Round int
- BlockID BlockID
- Type VoteType
- }
-
-
- type MessageDecision struct {
- Height int64
- Round int
- BlockID BlockID
- }
-
- type TriggerTimeout struct {
- Height int64
- Round int
- Duration Duration
- }
-
-
- type RoundStep int
-
- const (
- RoundStepUnknown RoundStep = iota
- RoundStepPropose
- RoundStepPrevote
- RoundStepPrecommit
- RoundStepCommit
- )
-
- type State struct {
- Height int64
- Round int
- Step RoundStep
- LockedValue BlockID
- LockedRound int
- ValidValue BlockID
- ValidRound int
- ValidatorId int
- ValidatorSetSize int
- }
-
- func proposer(height int64, round int) int {}
- func getValue() BlockID {}
-
- func Consensus(event Event, state State) (State, Message, TriggerTimeout) {
- msg = nil
- timeout = nil
- switch event := event.(type) {
- case EventNewHeight:
- if event.Height > state.Height {
- state.Height = event.Height
- state.Round = -1
- state.Step = RoundStepPropose
- state.LockedValue = nil
- state.LockedRound = -1
- state.ValidValue = nil
- state.ValidRound = -1
- state.ValidatorId = event.ValidatorId
- }
- return state, msg, timeout
-
- case EventNewRound:
- if event.Height == state.Height and event.Round > state.Round {
- state.Round = eventRound
- state.Step = RoundStepPropose
- if proposer(state.Height, state.Round) == state.ValidatorId {
- proposal = state.ValidValue
- if proposal == nil {
- proposal = getValue()
- }
- msg = MessageProposal { state.Height, state.Round, proposal, state.ValidRound }
- }
- timeout = TriggerTimeout { state.Height, state.Round, timeoutPropose(state.Round) }
- }
- return state, msg, timeout
-
- case EventProposal:
- if event.Height == state.Height and event.Round == state.Round and
- event.Sender == proposal(state.Height, state.Round) and state.Step == RoundStepPropose {
- if event.POLRound >= state.LockedRound or event.BlockID == state.BlockID or state.LockedRound == -1 {
- msg = MessageVote { state.Height, state.Round, event.BlockID, Prevote }
- }
- state.Step = RoundStepPrevote
- }
- return state, msg, timeout
-
- case TimeoutPropose:
- if event.Height == state.Height and event.Round == state.Round and state.Step == RoundStepPropose {
- msg = MessageVote { state.Height, state.Round, nil, Prevote }
- state.Step = RoundStepPrevote
- }
- return state, msg, timeout
-
- case Majority23PrevotesBlock:
- if event.Height == state.Height and event.Round == state.Round and state.Step >= RoundStepPrevote and event.Round > state.ValidRound {
- state.ValidRound = event.Round
- state.ValidValue = event.BlockID
- if state.Step == RoundStepPrevote {
- state.LockedRound = event.Round
- state.LockedValue = event.BlockID
- msg = MessageVote { state.Height, state.Round, event.BlockID, Precommit }
- state.Step = RoundStepPrecommit
- }
- }
- return state, msg, timeout
-
- case Majority23PrevotesAny:
- if event.Height == state.Height and event.Round == state.Round and state.Step == RoundStepPrevote {
- timeout = TriggerTimeout { state.Height, state.Round, timeoutPrevote(state.Round) }
- }
- return state, msg, timeout
-
- case TimeoutPrevote:
- if event.Height == state.Height and event.Round == state.Round and state.Step == RoundStepPrevote {
- msg = MessageVote { state.Height, state.Round, nil, Precommit }
- state.Step = RoundStepPrecommit
- }
- return state, msg, timeout
-
- case Majority23PrecommitBlock:
- if event.Height == state.Height {
- state.Step = RoundStepCommit
- state.LockedValue = event.BlockID
- }
- return state, msg, timeout
-
- case Majority23PrecommitAny:
- if event.Height == state.Height and event.Round == state.Round {
- timeout = TriggerTimeout { state.Height, state.Round, timeoutPrecommit(state.Round) }
- }
- return state, msg, timeout
-
- case TimeoutPrecommit:
- if event.Height == state.Height and event.Round == state.Round {
- state.Round = state.Round + 1
- }
- return state, msg, timeout
- }
- }
-
- func ConsensusExecutor() {
- proposal = nil
- votes = HeightVoteSet { Height: 1 }
- state = State {
- Height: 1
- Round: 0
- Step: RoundStepPropose
- LockedValue: nil
- LockedRound: -1
- ValidValue: nil
- ValidRound: -1
- }
-
- event = EventNewHeight {1, id}
- state, msg, timeout = Consensus(event, state)
-
- event = EventNewRound {state.Height, 0}
- state, msg, timeout = Consensus(event, state)
-
- if msg != nil {
- send msg
- }
-
- if timeout != nil {
- trigger timeout
- }
-
- for {
- select {
- case message := <- msgCh:
- switch msg := message.(type) {
- case MessageProposal:
-
- case MessageVote:
- if msg.Height == state.Height {
- newVote = votes.AddVote(msg)
- if newVote {
- switch msg.Type {
- case Prevote:
- prevotes = votes.Prevotes(msg.Round)
- if prevotes.WeakCertificate() and msg.Round > state.Round {
- event = EventNewRound { msg.Height, msg.Round }
- state, msg, timeout = Consensus(event, state)
- state = handleStateChange(state, msg, timeout)
- }
-
- if blockID, ok = prevotes.TwoThirdsMajority(); ok and blockID != nil {
- if msg.Round == state.Round and hasBlock(blockID) {
- event = Majority23PrevotesBlock { msg.Height, msg.Round, blockID }
- state, msg, timeout = Consensus(event, state)
- state = handleStateChange(state, msg, timeout)
- }
- if proposal != nil and proposal.POLRound == msg.Round and hasBlock(blockID) {
- event = EventProposal {
- Height: state.Height
- Round: state.Round
- BlockID: blockID
- POLRound: proposal.POLRound
- Sender: message.Sender
- }
- state, msg, timeout = Consensus(event, state)
- state = handleStateChange(state, msg, timeout)
- }
- }
-
- if prevotes.HasTwoThirdsAny() and msg.Round == state.Round {
- event = Majority23PrevotesAny { msg.Height, msg.Round, blockID }
- state, msg, timeout = Consensus(event, state)
- state = handleStateChange(state, msg, timeout)
- }
-
- case Precommit:
-
- }
- }
- }
- case timeout := <- timeoutCh:
-
- case block := <- blockCh:
-
- }
- }
- }
-
- func handleStateChange(state, msg, timeout) State {
- if state.Step == Commit {
- state = ExecuteBlock(state.LockedValue)
- }
- if msg != nil {
- send msg
- }
- if timeout != nil {
- trigger timeout
- }
- }
-
- ```
-
- ### Implementation roadmap
-
- * implement proposed implementation
- * replace currently scattered calls in `ConsensusState` with calls to the new
- `Consensus` function
- * rename `ConsensusState` to `ConsensusExecutor` to avoid confusion
- * propose design for improved separation and clear information flow between
- `ConsensusExecutor` and `ConsensusReactor`
-
- ## Status
-
- Draft.
-
- ## Consequences
-
- ### Positive
-
- - isolated implementation of the algorithm
- - improved testability - simpler to proof correctness
- - clearer separation of concerns - easier to reason
-
- ### Negative
-
- ### Neutral
|