Revision c5c088b1f87bf7fdb0fcb9d01b30da1e1264d0fa authored by tycho garen on 04 March 2022, 20:41:58 UTC, committed by tycho garen on 04 March 2022, 20:41:58 UTC
1 parent 1c7c3a0
Raw File
adr-030-consensus-refactor.md
# 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
back to top