Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of Tendermint #2185

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions consensus/tendermint/messages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package tendermint

// Todo: Signature over the messages needs to be handled somewhere. There are 2 options:
// 1. Add the signature to each message and extend the Validator Set interface to include VerifyMessageSignature
// method.
// 2. The P2P layer signs the message before gossiping to other validators and verifies the signature before passing
// the message to the consensus engine.
// The benefit of P2P layer handling the verification of the signature is the that the consensus layer can assume
// the message is from a validator in the validator set. However, this means that the P2P layer would need to be aware
// of the validator set and would need access to the blockchain which may not be a good idea.

type Message[V Hashable[H], H Hash, A Addr] interface {
Proposal[V, H, A] | Prevote[H, A] | Precommit[H, A]
}

type Proposal[V Hashable[H], H Hash, A Addr] struct {
Height uint
Round uint
ValidRound *uint
rianhughes marked this conversation as resolved.
Show resolved Hide resolved
Value *V

Sender A
}

type Prevote[H Hash, A Addr] struct {
Vote[H, A]
}

type Precommit[H Hash, A Addr] struct {
Vote[H, A]
}

type Vote[H Hash, A Addr] struct {
Height uint
Round uint
ID *H

Sender A
}

// messages keep tracks of all the proposals, prevotes, precommits by creating a map structure as follows:
// height->round->address->[]Message

// Todo: would the following representation of message be better:
//
// height -> round -> address -> ID -> Message
// How would we keep track of nil votes? In golan map key cannot be nil.
// It is not easy to calculate a zero value when dealing with generics.
type messages[V Hashable[H], H Hash, A Addr] struct {
proposals map[uint]map[uint]map[A][]Proposal[V, H, A]
prevotes map[uint]map[uint]map[A][]Prevote[H, A]
precommits map[uint]map[uint]map[A][]Precommit[H, A]
}

func newMessages[V Hashable[H], H Hash, A Addr]() messages[V, H, A] {
return messages[V, H, A]{
proposals: make(map[uint]map[uint]map[A][]Proposal[V, H, A]),
prevotes: make(map[uint]map[uint]map[A][]Prevote[H, A]),
precommits: make(map[uint]map[uint]map[A][]Precommit[H, A]),
}
}

// Todo: ensure duplicated messages are ignored.
rianhughes marked this conversation as resolved.
Show resolved Hide resolved
func (m *messages[V, H, A]) addProposal(p Proposal[V, H, A]) {
if _, ok := m.proposals[p.Height]; !ok {
m.proposals[p.Height] = make(map[uint]map[A][]Proposal[V, H, A])
}

if _, ok := m.proposals[p.Height][p.Round]; !ok {
m.proposals[p.Height][p.Round] = make(map[A][]Proposal[V, H, A])
}

sendersProposals, ok := m.proposals[p.Height][p.Round][p.Sender]
if !ok {
sendersProposals = []Proposal[V, H, A]{}
}

m.proposals[p.Height][p.Round][p.Sender] = append(sendersProposals, p)
rianhughes marked this conversation as resolved.
Show resolved Hide resolved
}

func (m *messages[V, H, A]) addPrevote(p Prevote[H, A]) {
if _, ok := m.prevotes[p.Height]; !ok {
m.prevotes[p.Height] = make(map[uint]map[A][]Prevote[H, A])
}

if _, ok := m.prevotes[p.Height][p.Round]; !ok {
m.prevotes[p.Height][p.Round] = make(map[A][]Prevote[H, A])
}

sendersPrevotes, ok := m.prevotes[p.Height][p.Round][p.Sender]
if !ok {
sendersPrevotes = []Prevote[H, A]{}
}

m.prevotes[p.Height][p.Round][p.Sender] = append(sendersPrevotes, p)
}

func (m *messages[V, H, A]) addPrecommit(p Precommit[H, A]) {
if _, ok := m.precommits[p.Height]; !ok {
m.precommits[p.Height] = make(map[uint]map[A][]Precommit[H, A])
}

if _, ok := m.precommits[p.Height][p.Round]; !ok {
m.precommits[p.Height][p.Round] = make(map[A][]Precommit[H, A])
}

sendersPrecommits, ok := m.precommits[p.Height][p.Round][p.Sender]
if !ok {
sendersPrecommits = []Precommit[H, A]{}
}

m.precommits[p.Height][p.Round][p.Sender] = append(sendersPrecommits, p)
}

func (m *messages[V, H, A]) allMessages(h, r uint) (map[A][]Proposal[V, H, A], map[A][]Prevote[H, A],
map[A][]Precommit[H, A],
) {
// Todo: Should they be copied?
return m.proposals[h][r], m.prevotes[h][r], m.precommits[h][r]
}

func (m *messages[V, H, A]) deleteHeightMessages(h uint) {
delete(m.proposals, h)
delete(m.prevotes, h)
delete(m.precommits, h)
}

func (m *messages[V, H, A]) deleteRoundMessages(h, r uint) {
delete(m.proposals[h], r)
delete(m.prevotes[h], r)
delete(m.precommits[h], r)
}
120 changes: 120 additions & 0 deletions consensus/tendermint/precommit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package tendermint

import (
"fmt"
"maps"
"slices"
)

func (t *Tendermint[V, H, A]) handlePrecommit(p Precommit[H, A]) {

Check failure on line 9 in consensus/tendermint/precommit.go

View workflow job for this annotation

GitHub Actions / lint

cyclomatic complexity 18 of func `(*Tendermint).handlePrecommit` is high (> 15) (gocyclo)
if p.Height < t.state.height {
return
}

if p.Height > t.state.height {
if p.Height-t.state.height > maxFutureHeight {
return
}

if p.Round > maxFutureRound {
return
}

t.futureMessagesMu.Lock()
defer t.futureMessagesMu.Unlock()
t.futureMessages.addPrecommit(p)
return
}

if p.Round > t.state.round {
if p.Round-t.state.round > maxFutureRound {
return
}

t.futureMessagesMu.Lock()
defer t.futureMessagesMu.Unlock()

t.futureMessages.addPrecommit(p)

/*
Check upon condition line 55:

55: upon f + 1 {∗, h_p, round, ∗, ∗} with round > round_p do
56: StartRound(round)
*/

t.line55(p.Round)
return
}

fmt.Println("got precommmit")
t.messages.addPrecommit(p)

proposalsForHR, _, precommitsForHR := t.messages.allMessages(p.Height, p.Round)

/*
Check the upon condition on line 49:

49: upon {PROPOSAL, h_p, r, v, *} from proposer(h_p, r) AND 2f + 1 {PRECOMMIT, h_p, r, id(v)} while decision_p[h_p] = nil do
50: if valid(v) then
51: decisionp[hp] = v
52: h_p ← h_p + 1
53: reset lockedRound_p, lockedValue_p, validRound_p and validValue_p to initial values and empty message log
54: StartRound(0)

Fetching the relevant proposal implies the sender of the proposal was the proposer for that
height and round. Also, since only the proposals with valid value are added to the message set, the
validity of the proposal can be skipped.

There is no need to check decision_p[h_p] = nil since it is implied that decision are made
sequentially, i.e. x, x+1, x+2... .

*/

if p.ID != nil {
var (
proposal *Proposal[V, H, A]
precommits []Precommit[H, A]
vals []A
)

for _, prop := range proposalsForHR[t.validators.Proposer(p.Height, p.Round)] {
if (*prop.Value).Hash() == *p.ID {
propCopy := prop
proposal = &propCopy
}
}

for addr, valPrecommits := range precommitsForHR {
for _, v := range valPrecommits {
if *v.ID == *p.ID {
precommits = append(precommits, v)
vals = append(vals, addr)
}
}
}
if proposal != nil && t.validatorSetVotingPower(vals) >= q(t.validators.TotalVotingPower(p.Height)) {
t.blockchain.Commit(t.state.height, *proposal.Value, precommits)

t.messages.deleteHeightMessages(t.state.height)
t.state.height++
t.startRound(0)

return
}
}

/*
Check the upon condition on line 47:

47: upon 2f + 1 {PRECOMMIT, h_p, round_p, ∗} for the first time do
48: schedule OnTimeoutPrecommit(h_p , round_p) to be executed after timeoutPrecommit(round_p)
*/

vals := slices.Collect(maps.Keys(precommitsForHR))
if p.Round == t.state.round && !t.state.line47Executed &&
t.validatorSetVotingPower(vals) >= q(t.validators.TotalVotingPower(p.Height)) {
t.scheduleTimeout(t.timeoutPrecommit(p.Round), precommit, p.Height, p.Round)
t.state.line47Executed = true
}
}
Loading
Loading