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, ready and easy to test for domain
experts.
Addresses:
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 inputMessage- signal what actions to perform
type Event intconst (EventUnknown Event = iotaEventProposalMajority23PrevotesBlockMajority23PrecommitBlockMajority23PrevotesAnyMajority23PrecommitAnyTimeoutNewRoundTimeoutProposeTimeoutPrevotesTimeoutPrecommit)type Message intconst (MeesageUnknown Message = iotaMessageProposalMessageVotesMessageDecision)type State struct {height uint64round uint64step uint64lockedValue 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:
func TestConsensusXXX(t *testing.T) {type expected struct {message Messagestate State}// Setup order of events, initial state and expectation.var (events = []struct {event Eventwant 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
type Event interface{}type EventNewHeight struct {Height int64ValidatorId int}type EventNewRound HeightAndRoundtype EventProposal struct {Height int64Round intTimestamp TimeBlockID BlockIDPOLRound intSender int}type Majority23PrevotesBlock struct {Height int64Round intBlockID BlockID}type Majority23PrecommitBlock struct {Height int64Round intBlockID BlockID}type HeightAndRound struct {Height int64Round int}type Majority23PrevotesAny HeightAndRoundtype Majority23PrecommitAny HeightAndRoundtype TimeoutPropose HeightAndRoundtype TimeoutPrevotes HeightAndRoundtype TimeoutPrecommit HeightAndRoundtype Message interface{}type MessageProposal struct {Height int64Round intBlockID BlockIDPOLRound int}type VoteType intconst (VoteTypeUnknown VoteType = iotaPrevotePrecommit)type MessageVote struct {Height int64Round intBlockID BlockIDType VoteType}type MessageDecision struct {Height int64Round intBlockID BlockID}type TriggerTimeout struct {Height int64Round intDuration Duration}type RoundStep intconst (RoundStepUnknown RoundStep = iotaRoundStepProposeRoundStepPrevoteRoundStepPrecommitRoundStepCommit)type State struct {Height int64Round intStep RoundStepLockedValue BlockIDLockedRound intValidValue BlockIDValidRound intValidatorId intValidatorSetSize int}func proposer(height int64, round int) int {}func getValue() BlockID {}func Consensus(event Event, state State) (State, Message, TriggerTimeout) {msg = niltimeout = nilswitch event := event.(type) {case EventNewHeight:if event.Height > state.Height {state.Height = event.Heightstate.Round = -1state.Step = RoundStepProposestate.LockedValue = nilstate.LockedRound = -1state.ValidValue = nilstate.ValidRound = -1state.ValidatorId = event.ValidatorId}return state, msg, timeoutcase EventNewRound:if event.Height == state.Height and event.Round > state.Round {state.Round = eventRoundstate.Step = RoundStepProposeif proposer(state.Height, state.Round) == state.ValidatorId {proposal = state.ValidValueif 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, timeoutcase EventProposal:if event.Height == state.Height and event.Round == state.Round andevent.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, timeoutcase 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, timeoutcase Majority23PrevotesBlock:if event.Height == state.Height and event.Round == state.Round and state.Step >= RoundStepPrevote and event.Round > state.ValidRound {state.ValidRound = event.Roundstate.ValidValue = event.BlockIDif state.Step == RoundStepPrevote {state.LockedRound = event.Roundstate.LockedValue = event.BlockIDmsg = MessageVote { state.Height, state.Round, event.BlockID, Precommit }state.Step = RoundStepPrecommit}}return state, msg, timeoutcase 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, timeoutcase 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, timeoutcase Majority23PrecommitBlock:if event.Height == state.Height {state.Step = RoundStepCommitstate.LockedValue = event.BlockID}return state, msg, timeoutcase Majority23PrecommitAny:if event.Height == state.Height and event.Round == state.Round {timeout = TriggerTimeout { state.Height, state.Round, timeoutPrecommit(state.Round) }}return state, msg, timeoutcase TimeoutPrecommit:if event.Height == state.Height and event.Round == state.Round {state.Round = state.Round + 1}return state, msg, timeout}}func ConsensusExecutor() {proposal = nilvotes = HeightVoteSet { Height: 1 }state = State {Height: 1Round: 0Step: RoundStepProposeLockedValue: nilLockedRound: -1ValidValue: nilValidRound: -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.HeightRound: state.RoundBlockID: blockIDPOLRound: proposal.POLRoundSender: 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
ConsensusStatewith calls to the newConsensusfunction - rename
ConsensusStatetoConsensusExecutorto avoid confusion - propose design for improved separation and clear information flow between
ConsensusExecutorandConsensusReactor
Status
Draft.
Consequences
Positive
- isolated implementation of the algorithm
- improved testability - simpler to proof correctness
- clearer separation of concerns - easier to reason
