Skip to content

Commit

Permalink
Initial implementation of Tendermint
Browse files Browse the repository at this point in the history
  • Loading branch information
IronGauntlets committed Oct 15, 2024
1 parent 71c7ca2 commit c0b79c3
Show file tree
Hide file tree
Showing 7 changed files with 1,985 additions and 0 deletions.
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
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.
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)
}

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

0 comments on commit c0b79c3

Please sign in to comment.