diff --git a/consensus/tendermint/messages.go b/consensus/tendermint/messages.go new file mode 100644 index 0000000000..7be5cc19f3 --- /dev/null +++ b/consensus/tendermint/messages.go @@ -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) +} diff --git a/consensus/tendermint/precommit.go b/consensus/tendermint/precommit.go new file mode 100644 index 0000000000..9ce5747513 --- /dev/null +++ b/consensus/tendermint/precommit.go @@ -0,0 +1,120 @@ +package tendermint + +import ( + "fmt" + "maps" + "slices" +) + +func (t *Tendermint[V, H, A]) handlePrecommit(p Precommit[H, A]) { + 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 + } +} diff --git a/consensus/tendermint/prevote.go b/consensus/tendermint/prevote.go new file mode 100644 index 0000000000..add19f078f --- /dev/null +++ b/consensus/tendermint/prevote.go @@ -0,0 +1,237 @@ +package tendermint + +import ( + "fmt" + "maps" + "slices" +) + +func (t *Tendermint[V, H, A]) handlePrevote(p Prevote[H, A]) { + 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.addPrevote(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.addPrevote(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 prevote") + t.messages.addPrevote(p) + + proposalsForHR, prevotesForHR, _ := t.messages.allMessages(p.Height, p.Round) + + /* + Check the upon condition on line 28: + + 28: upon {PROPOSAL, h_p, round_p, v, vr} from proposer(h_p, round_p) AND 2f + 1 {PREVOTE,h_p, vr, id(v)} while + step_p = propose ∧ (vr ≥ 0 ∧ vr < round_p) do + 29: if valid(v) ∧ (lockedRound_p ≤ vr ∨ lockedValue_p = v) then + 30: broadcast {PREVOTE, hp, round_p, id(v)} + 31: else + 32: broadcast {PREVOTE, hp, round_p, nil} + 33: step_p ← prevote + + 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. + + Calculating quorum of prevotes is more resource intensive than checking other condition on line 28, + therefore, it is checked in a subsequent if statement. + + */ + + if vr := p.Round; p.ID != nil && t.state.step == propose && vr >= 0 && vr < t.state.round { + cr := t.state.round + + proposalsForHCR, _, _ := t.messages.allMessages(p.Height, cr) + + var proposal *Proposal[V, H, A] + var vals []A + + for _, v := range proposalsForHCR[t.validators.Proposer(p.Height, p.Round)] { + if (*v.Value).Hash() == *p.ID && *v.ValidRound == vr { + proposal = &v + } + } + + for addr, valPrevotes := range prevotesForHR { + for _, v := range valPrevotes { + if *v.ID == *p.ID { + vals = append(vals, addr) + } + } + } + + if proposal != nil && t.validatorSetVotingPower(vals) >= q(t.validators.TotalVotingPower(p.Height)) { + vote := Prevote[H, A]{ + Vote: Vote[H, A]{ + Height: t.state.height, + Round: t.state.round, + ID: nil, + Sender: t.nodeAddr, + }, + } + + if *t.state.lockedRound >= vr || (*t.state.lockedValue).Hash() == *p.ID { + vote.ID = p.ID + } + + t.messages.addPrevote(vote) + t.broadcasters.PrevoteBroadcaster.Broadcast(vote) + t.state.step = prevote + + } + } + + if p.Round == t.state.round { + /* + + Check the upon condition on line 34: + + 34: upon 2f + 1 {PREVOTE, h_p, round_p, ∗} while step_p = prevote for the first time do + 35: schedule OnTimeoutPrevote(h_p, round_p) to be executed after timeoutPrevote(round_p) + */ + + vals := slices.Collect(maps.Keys(prevotesForHR)) + if !t.state.line34Executed && t.state.step == prevote && + t.validatorSetVotingPower(vals) >= q(t.validators.TotalVotingPower(p.Height)) { + t.scheduleTimeout(t.timeoutPrevote(p.Round), prevote, p.Height, p.Round) + t.state.line34Executed = true + } + + /* + Check the upon condition on line 44: + + 44: upon 2f + 1 {PREVOTE, h_p, round_p, nil} while step_p = prevote do + 45: broadcast {PRECOMMIT, hp, roundp, nil} + 46: step_p ← precommit + + Line 36 and 44 for a round are mutually exclusive. + + */ + + vals = []A{} + for addr, valPrevotes := range prevotesForHR { + for _, v := range valPrevotes { + if v.ID == nil { + vals = append(vals, addr) + } + } + } + + if t.state.step == prevote && t.validatorSetVotingPower(vals) >= q(t.validators.TotalVotingPower(p.Height)) { + vote := Precommit[H, A]{ + Vote: Vote[H, A]{ + Height: t.state.height, + Round: t.state.round, + ID: nil, + Sender: t.nodeAddr, + }, + } + + t.messages.addPrecommit(vote) + t.broadcasters.PrecommitBroadcaster.Broadcast(vote) + t.state.step = precommit + } + + /* + Check upon condition on line 36: + + 36: upon {PROPOSAL, h_p, round_p, v, ∗} from proposer(h_p, round_p) AND 2f + 1 {PREVOTE, h_p, round_p, id(v)} while + valid(v) ∧ step_p ≥ prevote for the first time do + 37: if step_p = prevote then + 38: lockedValue_p ← v + 39: lockedRound_p ← round_p + 40: broadcast {PRECOMMIT, h_p, round_p, id(v))} + 41: step_p ← precommit + 42: validValue_p ← v + 43: validRound_p ← round_p + + 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. + + Calculating quorum of prevotes is more resource intensive than checking other condition on line 36, + therefore, it is checked in a subsequent if statement. + + */ + + if !t.state.line36Executed && t.state.step >= prevote { + var proposal *Proposal[V, H, A] + vals = []A{} + + for _, v := range proposalsForHR[t.validators.Proposer(p.Height, p.Round)] { + if (*v.Value).Hash() == *p.ID { + vCopy := v + proposal = &vCopy + } + } + + for addr, valPrevotes := range prevotesForHR { + for _, v := range valPrevotes { + if *v.ID == *p.ID { + vals = append(vals, addr) + } + } + } + + if proposal != nil && t.validatorSetVotingPower(vals) >= q(t.validators.TotalVotingPower(p.Height)) { + cr := t.state.round + + if t.state.step == prevote { + t.state.lockedValue = proposal.Value + t.state.lockedRound = &cr + + vote := Precommit[H, A]{ + Vote: Vote[H, A]{ + Height: t.state.height, + Round: t.state.round, + ID: p.ID, + Sender: t.nodeAddr, + }, + } + + t.messages.addPrecommit(vote) + t.broadcasters.PrecommitBroadcaster.Broadcast(vote) + t.state.step = precommit + } + + t.state.validValue = proposal.Value + t.state.validRound = &cr + t.state.line36Executed = true + } + } + } +} diff --git a/consensus/tendermint/propose.go b/consensus/tendermint/propose.go new file mode 100644 index 0000000000..7c95326bcb --- /dev/null +++ b/consensus/tendermint/propose.go @@ -0,0 +1,247 @@ +package tendermint + +import ( + "fmt" +) + +//nolint:funlen +func (t *Tendermint[V, H, A]) handleProposal(p Proposal[V, H, A]) { + 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.addProposal(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.addProposal(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 + } + + // The code below shouldn't panic because it is expected Proposal is well-formed. However, there need to be a way to + // distinguish between nil and zero value. This is expected to be handled by the p2p layer. + vID := (*p.Value).Hash() + fmt.Println("got proposal with valueID and validRound", vID, p.ValidRound) + validProposal := t.application.Valid(*p.Value) + proposalFromProposer := p.Sender == t.validators.Proposer(p.Height, p.Round) + vr := p.ValidRound + + if validProposal { + // Add the proposal to the message set even if the sender is not the proposer, + // this is because of slahsing purposes + t.messages.addProposal(p) + } + + _, prevotesForHR, 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) + + 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... . The validity of the proposal value can be checked in the same if + statement since there is no else statement. + */ + + var precommits []Precommit[H, A] + var vals []A + + for addr, valPrecommits := range precommitsForHR { + for _, p := range valPrecommits { + if *p.ID == vID { + precommits = append(precommits, p) + vals = append(vals, addr) + } + } + } + + if validProposal && proposalFromProposer && + t.validatorSetVotingPower(vals) >= q(t.validators.TotalVotingPower(p.Height)) { + // After committing the block, how the new height and round is started needs to be coordinated + // with the synchronisation process. + t.blockchain.Commit(t.state.height, *p.Value, precommits) + + t.messages.deleteHeightMessages(t.state.height) + t.state.height++ + t.startRound(0) + + return + } + + if p.Round < t.state.round { + // Except line 49 all other upon condition which refer to the proposals expect to be acted upon + // when the current round is equal to the proposal's round. + return + } + + /* + Check the upon condition on line 22: + + 22: upon {PROPOSAL, h_p, round_p, v, nil} from proposer(h_p, round_p) while step_p = propose do + 23: if valid(v) ∧ (lockedRound_p = −1 ∨ lockedValue_p = v) then + 24: broadcast {PREVOTE, h_p, round_p, id(v)} + 25: else + 26: broadcast {PREVOTE, h_p, round_p, nil} + 27: step_p ← prevote + + The implementation uses nil as -1 to avoid using int type. + + Since the value's id is expected to be unique the id can be used to compare the values. + + */ + + if vr == nil && proposalFromProposer && t.state.step == propose { + vote := Prevote[H, A]{ + Vote: Vote[H, A]{ + Height: t.state.height, + Round: t.state.round, + ID: nil, + Sender: t.nodeAddr, + }, + } + + if validProposal && (t.state.lockedRound == nil || (*t.state.lockedValue).Hash() == vID) { + vote.ID = &vID + } + + t.messages.addPrevote(vote) + t.broadcasters.PrevoteBroadcaster.Broadcast(vote) + t.state.step = prevote + } + + /* + Check the upon condition on line 28: + + 28: upon {PROPOSAL, h_p, round_p, v, vr} from proposer(h_p, round_p) AND 2f + 1 {PREVOTE,h_p, vr, id(v)} while + step_p = propose ∧ (vr ≥ 0 ∧ vr < round_p) do + 29: if valid(v) ∧ (lockedRound_p ≤ vr ∨ lockedValue_p = v) then + 30: broadcast {PREVOTE, hp, round_p, id(v)} + 31: else + 32: broadcast {PREVOTE, hp, round_p, nil} + 33: step_p ← prevote + + Ideally the condition on line 28 would be checked in a single if statement, however, + this cannot be done because valid round needs to be non-nil before the prevotes are fetched. + */ + + if vr != nil && proposalFromProposer && t.state.step == propose && *vr >= uint(0) && *vr < t.state.round { + _, prevotesForHVr, _ := t.messages.allMessages(p.Height, *vr) + + vals = []A{} + for addr, valPrevotes := range prevotesForHVr { + for _, p := range valPrevotes { + if *p.ID == vID { + vals = append(vals, addr) + } + } + } + + if t.validatorSetVotingPower(vals) >= q(t.validators.TotalVotingPower(p.Height)) { + vote := Prevote[H, A]{ + Vote: Vote[H, A]{ + Height: t.state.height, + Round: t.state.round, + ID: nil, + Sender: t.nodeAddr, + }, + } + + if validProposal && (*t.state.lockedRound >= *vr || (*t.state.lockedValue).Hash() == vID) { + vote.ID = &vID + } + + t.messages.addPrevote(vote) + t.broadcasters.PrevoteBroadcaster.Broadcast(vote) + t.state.step = prevote + } + } + + /* + Check upon condition on line 36: + + 36: upon {PROPOSAL, h_p, round_p, v, ∗} from proposer(h_p, round_p) AND 2f + 1 {PREVOTE, h_p, round_p, id(v)} while + valid(v) ∧ step_p ≥ prevote for the first time do + 37: if step_p = prevote then + 38: lockedValue_p ← v + 39: lockedRound_p ← round_p + 40: broadcast {PRECOMMIT, h_p, round_p, id(v))} + 41: step_p ← precommit + 42: validValue_p ← v + 43: validRound_p ← round_p + + The condition on line 36 can should be checked in a single if statement, however, + checking for quroum is more resource intensive than other conditions therefore they are checked + first. + */ + + if validProposal && proposalFromProposer && !t.state.line36Executed && t.state.step >= prevote { + vals = []A{} + for addr, valPrevotes := range prevotesForHR { + for _, v := range valPrevotes { + if *v.ID == vID { + vals = append(vals, addr) + } + } + } + + if t.validatorSetVotingPower(vals) >= q(t.validators.TotalVotingPower(p.Height)) { + cr := t.state.round + + if t.state.step == prevote { + t.state.lockedValue = p.Value + t.state.lockedRound = &cr + + vote := Precommit[H, A]{ + Vote: Vote[H, A]{ + Height: t.state.height, + Round: t.state.round, + ID: &vID, + Sender: t.nodeAddr, + }, + } + + t.messages.addPrecommit(vote) + t.broadcasters.PrecommitBroadcaster.Broadcast(vote) + t.state.step = precommit + } + + t.state.validValue = p.Value + t.state.validRound = &cr + t.state.line36Executed = true + } + } +} diff --git a/consensus/tendermint/propose_test.go b/consensus/tendermint/propose_test.go new file mode 100644 index 0000000000..af0dfc9b01 --- /dev/null +++ b/consensus/tendermint/propose_test.go @@ -0,0 +1,536 @@ +package tendermint + +import ( + "slices" + "testing" + "time" + + "github.com/NethermindEth/juno/core/felt" + "github.com/NethermindEth/juno/utils" + "github.com/stretchr/testify/assert" +) + +func TestPropose(t *testing.T) { + nodeAddr := new(felt.Felt).SetBytes([]byte("my node address")) + val2, val3, val4 := new(felt.Felt).SetUint64(2), new(felt.Felt).SetUint64(3), new(felt.Felt).SetUint64(4) + tm := func(r uint) time.Duration { return time.Second } + + t.Run("Line 55 (Proposal): Start round r' when f+1 future round messages are received from round r'", func(t *testing.T) { + listeners, broadcasters := testListenersAndBroadcasters() + app, chain, vals := newApp(), newChain(), newVals() + + vals.addValidator(*val2) + vals.addValidator(*val3) + vals.addValidator(*val4) + vals.addValidator(*nodeAddr) + + algo := New[value, felt.Felt, felt.Felt](*nodeAddr, app, chain, vals, listeners, broadcasters, tm, tm, tm) + + expectedHeight := uint(0) + rPrime := uint(4) + round4Value := value(10) + val2Proposal := Proposal[value, felt.Felt, felt.Felt]{ + Height: expectedHeight, + Round: rPrime, + ValidRound: nil, + Value: &round4Value, + Sender: *val2, + } + + val3Prevote := Prevote[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: expectedHeight, + Round: rPrime, + ID: utils.Ptr(round4Value.Hash()), + Sender: *val3, + }, + } + + algo.messages.addPrevote(val3Prevote) + proposalListener := listeners.ProposalListener.(*senderAndReceiver[Proposal[value, felt.Felt, felt.Felt], + value, felt.Felt, felt.Felt]) + proposalListener.send(val2Proposal) + + algo.Start() + time.Sleep(1 * time.Millisecond) + algo.Stop() + + assert.Equal(t, 1, len(algo.messages.proposals[expectedHeight][rPrime][*val2])) + assert.Equal(t, 1, len(algo.messages.prevotes[expectedHeight][rPrime][*val3])) + assert.Equal(t, val3Prevote, algo.messages.prevotes[expectedHeight][rPrime][*val3][0]) + + assert.Equal(t, propose, algo.state.step) + assert.Equal(t, expectedHeight, algo.state.height) + assert.Equal(t, rPrime, algo.state.round) + }) + + t.Run("Line 55 (Prevote): Start round r' when f+1 future round messages are received from round r'", func(t *testing.T) { + listeners, broadcasters := testListenersAndBroadcasters() + app, chain, vals := newApp(), newChain(), newVals() + + vals.addValidator(*val2) + vals.addValidator(*val3) + vals.addValidator(*val4) + vals.addValidator(*nodeAddr) + + algo := New[value, felt.Felt, felt.Felt](*nodeAddr, app, chain, vals, listeners, broadcasters, tm, tm, tm) + + expectedHeight := uint(0) + rPrime := uint(4) + round4Value := value(10) + val2Proposal := Proposal[value, felt.Felt, felt.Felt]{ + Height: expectedHeight, + Round: rPrime, + ValidRound: nil, + Value: &round4Value, + Sender: *val2, + } + + val3Prevote := Prevote[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: expectedHeight, + Round: rPrime, + ID: utils.Ptr(round4Value.Hash()), + Sender: *val3, + }, + } + + algo.messages.addProposal(val2Proposal) + prevoteListener := listeners.PrevoteListener.(*senderAndReceiver[Prevote[felt.Felt, felt.Felt], value, + felt.Felt, felt.Felt]) + prevoteListener.send(val3Prevote) + + algo.Start() + time.Sleep(1 * time.Millisecond) + algo.Stop() + + assert.Equal(t, 1, len(algo.messages.proposals[expectedHeight][rPrime][*val2])) + assert.Equal(t, 1, len(algo.messages.prevotes[expectedHeight][rPrime][*val3])) + assert.Equal(t, val3Prevote, algo.messages.prevotes[expectedHeight][rPrime][*val3][0]) + + assert.Equal(t, propose, algo.state.step) + assert.Equal(t, expectedHeight, algo.state.height) + assert.Equal(t, rPrime, algo.state.round) + }) + + t.Run("Line 55 (Precommit): Start round r' when f+1 future round messages are received from round r'", func(t *testing.T) { + listeners, broadcasters := testListenersAndBroadcasters() + app, chain, vals := newApp(), newChain(), newVals() + + vals.addValidator(*val2) + vals.addValidator(*val3) + vals.addValidator(*val4) + vals.addValidator(*nodeAddr) + + algo := New[value, felt.Felt, felt.Felt](*nodeAddr, app, chain, vals, listeners, broadcasters, tm, tm, tm) + + expectedHeight := uint(0) + rPrime := uint(4) + round4Value := value(10) + val2Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: expectedHeight, + Round: rPrime, + ID: utils.Ptr(round4Value.Hash()), + Sender: *val2, + }, + } + + val3Prevote := Prevote[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: expectedHeight, + Round: rPrime, + ID: utils.Ptr(round4Value.Hash()), + Sender: *val3, + }, + } + + algo.messages.addPrevote(val3Prevote) + prevoteListener := listeners.PrecommitListener.(*senderAndReceiver[Precommit[felt.Felt, felt.Felt], value, + felt.Felt, felt.Felt]) + prevoteListener.send(val2Precommit) + + algo.Start() + time.Sleep(1 * time.Millisecond) + algo.Stop() + + assert.Equal(t, 1, len(algo.messages.precommits[expectedHeight][rPrime][*val2])) + assert.Equal(t, 1, len(algo.messages.prevotes[expectedHeight][rPrime][*val3])) + assert.Equal(t, val3Prevote, algo.messages.prevotes[expectedHeight][rPrime][*val3][0]) + + assert.Equal(t, propose, algo.state.step) + assert.Equal(t, expectedHeight, algo.state.height) + assert.Equal(t, rPrime, algo.state.round) + }) + + t.Run("Line 47: schedule timeout precommit", func(t *testing.T) { + listeners, broadcasters := testListenersAndBroadcasters() + nodeAddr := new(felt.Felt).SetBytes([]byte("my node address")) + app, chain, vals := newApp(), newChain(), newVals() + + vals.addValidator(*val2) + vals.addValidator(*val3) + vals.addValidator(*val4) + vals.addValidator(*nodeAddr) + + algo := New[value, felt.Felt, felt.Felt](*nodeAddr, app, chain, vals, listeners, broadcasters, tm, tm, tm) + + val2Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: 0, + Round: 0, + ID: utils.Ptr(value(10).Hash()), + Sender: *val2, + }, + } + val3Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: 0, + Round: 0, + ID: nil, + Sender: *val3, + }, + } + val4Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: 0, + Round: 0, + ID: nil, + Sender: *val4, + }, + } + + algo.messages.addPrecommit(val2Precommit) + algo.messages.addPrecommit(val3Precommit) + + precommitListner := listeners.PrecommitListener.(*senderAndReceiver[Precommit[felt.Felt, felt.Felt], value, + felt.Felt, felt.Felt]) + precommitListner.send(val4Precommit) + + algo.Start() + time.Sleep(1 * time.Millisecond) + algo.Stop() + + assert.Equal(t, 2, len(algo.scheduledTms)) + scheduledTm := algo.scheduledTms[1] + + assert.Equal(t, precommit, scheduledTm.s) + assert.Equal(t, uint(0), scheduledTm.h) + assert.Equal(t, uint(0), scheduledTm.r) + + assert.True(t, algo.state.line47Executed) + assert.Equal(t, propose, algo.state.step) + assert.Equal(t, uint(0), algo.state.height) + assert.Equal(t, uint(0), algo.state.round) + }) + + t.Run("Line 47: don't schedule timeout precommit multiple times", func(t *testing.T) { + listeners, broadcasters := testListenersAndBroadcasters() + nodeAddr := new(felt.Felt).SetBytes([]byte("my node address")) + app, chain, vals := newApp(), newChain(), newVals() + + vals.addValidator(*val2) + vals.addValidator(*val3) + vals.addValidator(*val4) + vals.addValidator(*nodeAddr) + + algo := New[value, felt.Felt, felt.Felt](*nodeAddr, app, chain, vals, listeners, broadcasters, tm, tm, tm) + + nodePrecommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: 0, + Round: 0, + ID: nil, + Sender: *nodeAddr, + }, + } + val2Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: 0, + Round: 0, + ID: utils.Ptr(value(10).Hash()), + Sender: *val2, + }, + } + val3Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: 0, + Round: 0, + ID: nil, + Sender: *val3, + }, + } + val4Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: 0, + Round: 0, + ID: nil, + Sender: *val4, + }, + } + + algo.messages.addPrecommit(val2Precommit) + algo.messages.addPrecommit(val3Precommit) + + precommitListner := listeners.PrecommitListener.(*senderAndReceiver[Precommit[felt.Felt, felt.Felt], value, + felt.Felt, felt.Felt]) + precommitListner.send(val4Precommit) + precommitListner.send(nodePrecommit) + + algo.Start() + time.Sleep(1 * time.Millisecond) + algo.Stop() + + assert.Equal(t, 2, len(algo.scheduledTms)) + scheduledTm := algo.scheduledTms[1] + + assert.Equal(t, precommit, scheduledTm.s) + assert.Equal(t, uint(0), scheduledTm.h) + assert.Equal(t, uint(0), scheduledTm.r) + + assert.True(t, algo.state.line47Executed) + assert.Equal(t, propose, algo.state.step) + assert.Equal(t, uint(0), algo.state.height) + assert.Equal(t, uint(0), algo.state.round) + }) + + t.Run("OnTimeoutPrecommit: move to next round", func(t *testing.T) { + listeners, broadcasters := testListenersAndBroadcasters() + nodeAddr := new(felt.Felt).SetBytes([]byte("my node address")) + app, chain, vals := newApp(), newChain(), newVals() + tmPrecommit := func(r uint) time.Duration { return time.Nanosecond } + + vals.addValidator(*val2) + vals.addValidator(*val3) + vals.addValidator(*val4) + vals.addValidator(*nodeAddr) + + algo := New[value, felt.Felt, felt.Felt](*nodeAddr, app, chain, vals, listeners, broadcasters, tm, tm, tmPrecommit) + + val2Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: 0, + Round: 0, + ID: utils.Ptr(value(10).Hash()), + Sender: *val2, + }, + } + val3Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: 0, + Round: 0, + ID: nil, + Sender: *val3, + }, + } + val4Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: 0, + Round: 0, + ID: nil, + Sender: *val4, + }, + } + + algo.messages.addPrecommit(val2Precommit) + algo.messages.addPrecommit(val3Precommit) + + precommitListner := listeners.PrecommitListener.(*senderAndReceiver[Precommit[felt.Felt, felt.Felt], value, + felt.Felt, felt.Felt]) + precommitListner.send(val4Precommit) + + algo.Start() + time.Sleep(1 * time.Millisecond) + algo.Stop() + + assert.Equal(t, 2, len(algo.scheduledTms)) + scheduledTm := algo.scheduledTms[1] + + assert.False(t, algo.state.line47Executed) + assert.Equal(t, propose, algo.state.step) + assert.Equal(t, uint(0), algo.state.height) + assert.Equal(t, uint(1), algo.state.round) + + assert.Equal(t, propose, scheduledTm.s) + assert.Equal(t, uint(0), scheduledTm.h) + assert.Equal(t, uint(1), scheduledTm.r) + }) + + t.Run("Line 49 (Proposal): commit the value", func(t *testing.T) { + listeners, broadcasters := testListenersAndBroadcasters() + nodeAddr := new(felt.Felt).SetBytes([]byte("my node address")) + app, chain, vals := newApp(), newChain(), newVals() + + vals.addValidator(*val2) + vals.addValidator(*val3) + vals.addValidator(*val4) + vals.addValidator(*nodeAddr) + + algo := New[value, felt.Felt, felt.Felt](*nodeAddr, app, chain, vals, listeners, broadcasters, tm, tm, tm) + + h, r := uint(0), uint(0) + + val := app.Value() + vID := val.Hash() + + val2Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: h, + Round: r, + ID: &vID, + Sender: *val2, + }, + } + val3Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: h, + Round: r, + ID: &vID, + Sender: *val3, + }, + } + val4Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: h, + Round: r, + ID: &vID, + Sender: *val4, + }, + } + + // The node has received all the precommits but has received the corresponding proposal + algo.messages.addPrecommit(val2Precommit) + algo.messages.addPrecommit(val3Precommit) + algo.messages.addPrecommit(val4Precommit) + + // since val2 is the proposer of round 0, the proposal arrives after the precommits + val2Proposal := Proposal[value, felt.Felt, felt.Felt]{ + Height: h, + Round: r, + ValidRound: nil, + Value: &val, + Sender: *val2, + } + + proposalListener := listeners.ProposalListener.(*senderAndReceiver[Proposal[value, felt.Felt, felt.Felt], + value, felt.Felt, felt.Felt]) + proposalListener.send(val2Proposal) + + algo.Start() + time.Sleep(1 * time.Millisecond) + algo.Stop() + + assert.Equal(t, 2, len(algo.scheduledTms)) + scheduledTm := algo.scheduledTms[1] + + assert.Equal(t, propose, algo.state.step) + assert.Equal(t, uint(1), algo.state.height) + assert.Equal(t, uint(0), algo.state.round) + + assert.Equal(t, propose, scheduledTm.s) + assert.Equal(t, uint(1), scheduledTm.h) + assert.Equal(t, uint(0), scheduledTm.r) + + precommits := []Precommit[felt.Felt, felt.Felt]{val2Precommit, val3Precommit, val4Precommit} + assert.Equal(t, chain.decision[0], val) + for _, p := range chain.decisionCertificates[0] { + assert.True(t, slices.Contains(precommits, p)) + } + + assert.Equal(t, 0, len(algo.messages.proposals)) + assert.Equal(t, 0, len(algo.messages.prevotes)) + assert.Equal(t, 0, len(algo.messages.precommits)) + }) + + t.Run("Line 49 (Precommit): commit the value", func(t *testing.T) { + listeners, broadcasters := testListenersAndBroadcasters() + nodeAddr := new(felt.Felt).SetBytes([]byte("my node address")) + app, chain, vals := newApp(), newChain(), newVals() + + vals.addValidator(*val2) + vals.addValidator(*val3) + vals.addValidator(*val4) + vals.addValidator(*nodeAddr) + + algo := New[value, felt.Felt, felt.Felt](*nodeAddr, app, chain, vals, listeners, broadcasters, tm, tm, tm) + + h, r := uint(0), uint(0) + + val := app.Value() + vID := val.Hash() + + val2Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: h, + Round: r, + ID: &vID, + Sender: *val2, + }, + } + val2Proposal := Proposal[value, felt.Felt, felt.Felt]{ + Height: h, + Round: r, + ValidRound: nil, + Value: &val, + Sender: *val2, + } + val3Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: h, + Round: r, + ID: &vID, + Sender: *val3, + }, + } + + // The node has received all the precommits but has received the corresponding proposal + algo.messages.addPrecommit(val2Precommit) + algo.messages.addProposal(val2Proposal) + algo.messages.addPrecommit(val3Precommit) + + val4Precommit := Precommit[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: h, + Round: r, + ID: &vID, + Sender: *val4, + }, + } + + precommitListner := listeners.PrecommitListener.(*senderAndReceiver[Precommit[felt.Felt, felt.Felt], + value, felt.Felt, felt.Felt]) + precommitListner.send(val4Precommit) + + algo.Start() + time.Sleep(1 * time.Millisecond) + algo.Stop() + + assert.Equal(t, 2, len(algo.scheduledTms)) + scheduledTm := algo.scheduledTms[1] + + assert.Equal(t, propose, algo.state.step) + assert.Equal(t, uint(1), algo.state.height) + assert.Equal(t, uint(0), algo.state.round) + + assert.Equal(t, propose, scheduledTm.s) + assert.Equal(t, uint(1), scheduledTm.h) + assert.Equal(t, uint(0), scheduledTm.r) + + precommits := []Precommit[felt.Felt, felt.Felt]{val2Precommit, val3Precommit, val4Precommit} + assert.Equal(t, chain.decision[0], val) + for _, p := range chain.decisionCertificates[0] { + assert.True(t, slices.Contains(precommits, p)) + } + + assert.Equal(t, 0, len(algo.messages.proposals)) + assert.Equal(t, 0, len(algo.messages.prevotes)) + assert.Equal(t, 0, len(algo.messages.precommits)) + }) + + t.Run("Line 22: receive a new proposal before timeout expiry", func(t *testing.T) { + }) + // + //t.Run("Line 28: receive an old proposal before timeout expiry", func(t *testing.T) { + // + //}) +} diff --git a/consensus/tendermint/tendermint.go b/consensus/tendermint/tendermint.go new file mode 100644 index 0000000000..5133cbef09 --- /dev/null +++ b/consensus/tendermint/tendermint.go @@ -0,0 +1,431 @@ +package tendermint + +import ( + "fmt" + "maps" + "slices" + "sync" + "time" + + "github.com/NethermindEth/juno/core/felt" +) + +type step uint + +const ( + propose step = iota + prevote + precommit +) + +const ( + maxFutureHeight = uint(5) + maxFutureRound = uint(5) +) + +type timeoutFn func(round uint) time.Duration + +type Addr interface { + // Ethereum Addresses are 20 bytes + ~[20]byte | felt.Felt +} + +type Hash interface { + ~[32]byte | felt.Felt +} + +// Hashable's Hash() is used as ID() +type Hashable[H Hash] interface { + Hash() H +} + +type Application[V Hashable[H], H Hash] interface { + // Value returns the value to the Tendermint consensus algorith which can be proposed to other validators. + Value() V + + // Valid returns true if the provided value is valid according to the application context. + Valid(V) bool +} + +type Blockchain[V Hashable[H], H Hash, A Addr] interface { + // Height return the current blockchain height + Height() uint + + // Commit is called by Tendermint when a block has been decided on and can be committed to the DB. + Commit(uint, V, []Precommit[H, A]) +} + +type Validators[A Addr] interface { + // TotalVotingPower represents N which is required to calculate the thresholds. + TotalVotingPower(height uint) uint + + // ValidatorVotingPower returns the voting power of the a single validator. This is also required to implement + // various thresholds. The assumption is that a single validator cannot have voting power more than f. + ValidatorVotingPower(validatorAddr A) uint + + // Proposer returns the proposer of the current round and height. + Proposer(height, round uint) A +} + +type Slasher[M Message[V, H, A], V Hashable[H], H Hash, A Addr] interface { + // Equivocation informs the slasher that a validator has sent conflicting messages. Thus it can decide whether to + // slash the validator and by how much. + Equivocation(msgs ...M) +} + +type Listener[M Message[V, H, A], V Hashable[H], H Hash, A Addr] interface { + // Listen would return consensus messages to Tendermint which are set // by the validator set. + Listen() <-chan M +} + +type Broadcaster[M Message[V, H, A], V Hashable[H], H Hash, A Addr] interface { + // Broadcast will broadcast the message to the whole validator set. The function should not be blocking. + Broadcast(msg M) + + // SendMsg would send a message to a specific validator. This would be required for helping send resquest and + // response message to help a specifc validator to catch up. + SendMsg(validatorAddr A, msg M) +} + +type Listeners[V Hashable[H], H Hash, A Addr] struct { + ProposalListener Listener[Proposal[V, H, A], V, H, A] + PrevoteListener Listener[Prevote[H, A], V, H, A] + PrecommitListener Listener[Precommit[H, A], V, H, A] +} + +type Broadcasters[V Hashable[H], H Hash, A Addr] struct { + ProposalBroadcaster Broadcaster[Proposal[V, H, A], V, H, A] + PrevoteBroadcaster Broadcaster[Prevote[H, A], V, H, A] + PrecommitBroadcaster Broadcaster[Precommit[H, A], V, H, A] +} + +type Tendermint[V Hashable[H], H Hash, A Addr] struct { + nodeAddr A + + state state[V, H] // Todo: Does state need to be protected? + + messages messages[V, H, A] + futureMessages messages[V, H, A] + + futureMessagesMu *sync.Mutex + + timeoutPropose timeoutFn + timeoutPrevote timeoutFn + timeoutPrecommit timeoutFn + + application Application[V, H] + blockchain Blockchain[V, H, A] + validators Validators[A] + + listeners Listeners[V, H, A] + broadcasters Broadcasters[V, H, A] + + scheduledTms []timeout + timeoutsCh chan timeout + + // Future round messages are sent to the loop through the following channels + proposalsCh chan Proposal[V, H, A] + prevotesCh chan Prevote[H, A] + precommitsCh chan Precommit[H, A] + + wg sync.WaitGroup + quit chan struct{} +} + +type state[V Hashable[H], H Hash] struct { + height uint + round uint + step step + + lockedValue *V + validValue *V + + // The default value of lockedRound and validRound is -1. However, using int for one value is not good use of space, + // therefore, uint is used and nil would represent -1. + lockedRound *uint + validRound *uint + + // The following are round level variable therefore when a round changes they must be reset. + line34Executed bool + line36Executed bool + line47Executed bool +} + +func New[V Hashable[H], H Hash, A Addr](addr A, app Application[V, H], chain Blockchain[V, H, A], vals Validators[A], + listeners Listeners[V, H, A], broadcasters Broadcasters[V, H, A], tmPropose, tmPrevote, tmPrecommit timeoutFn, +) *Tendermint[V, H, A] { + return &Tendermint[V, H, A]{ + nodeAddr: addr, + state: state[V, H]{height: chain.Height()}, + messages: newMessages[V, H, A](), + futureMessages: newMessages[V, H, A](), + futureMessagesMu: &sync.Mutex{}, + timeoutPropose: tmPropose, + timeoutPrevote: tmPrevote, + timeoutPrecommit: tmPrecommit, + application: app, + blockchain: chain, + validators: vals, + listeners: listeners, + broadcasters: broadcasters, + scheduledTms: make([]timeout, 0), + timeoutsCh: make(chan timeout), + proposalsCh: make(chan Proposal[V, H, A]), + prevotesCh: make(chan Prevote[H, A]), + precommitsCh: make(chan Precommit[H, A]), + quit: make(chan struct{}), + } +} + +func (t *Tendermint[V, H, A]) Start() { + t.wg.Add(1) + go func() { + defer t.wg.Done() + + t.startRound(0) + + // Todo: check message signature everytime a message is received. + // For the time being it can be assumed the signature is correct. + + i := 0 + for { + fmt.Println("iteration: ", i) + select { + case <-t.quit: + fmt.Println("quit", t.state.step) + return + case tm := <-t.timeoutsCh: + // Handling of timeouts is priorities over messages + fmt.Println("Inside timeout case", tm) + switch tm.s { + case propose: + t.OnTimeoutPropose(tm.h, tm.r) + case prevote: + t.OnTimeoutPrevote(tm.h, tm.r) + case precommit: + t.OnTimeoutPrecommit(tm.h, tm.r) + } + + i := slices.Index(t.scheduledTms, tm) + t.scheduledTms = slices.Delete(t.scheduledTms, i, i+1) + case p := <-t.proposalsCh: + t.handleProposal(p) + case p := <-t.listeners.ProposalListener.Listen(): + t.handleProposal(p) + case p := <-t.prevotesCh: + t.handlePrevote(p) + case p := <-t.listeners.PrevoteListener.Listen(): + t.handlePrevote(p) + case p := <-t.precommitsCh: + t.handlePrecommit(p) + case p := <-t.listeners.PrecommitListener.Listen(): + t.handlePrecommit(p) + } + i++ + } + }() +} + +func (t *Tendermint[V, H, A]) Stop() { + fmt.Println("Stopping Tendermint...") + close(t.quit) + for _, tm := range t.scheduledTms { + tm.Stop() + } + fmt.Println("Waiting fo the Tendermint loop to exit") + t.wg.Wait() +} + +func (t *Tendermint[V, H, A]) startRound(r uint) { + fmt.Println("Inside start round", r) + if r != 0 && r <= t.state.round { + return + } + + t.state.round = r + t.state.step = propose + + t.state.line34Executed = false + t.state.line36Executed = false + t.state.line47Executed = false + + if p := t.validators.Proposer(t.state.height, r); p == t.nodeAddr { + var proposalValue *V + if t.state.validValue != nil { + proposalValue = t.state.validValue + } else { + v := t.application.Value() + proposalValue = &v + } + proposalMessage := Proposal[V, H, A]{ + Height: t.state.height, + Round: r, + ValidRound: t.state.validRound, + Value: proposalValue, + Sender: t.nodeAddr, + } + + t.messages.addProposal(proposalMessage) + fmt.Println("Broadcasting the proposal") + t.broadcasters.ProposalBroadcaster.Broadcast(proposalMessage) + } else { + t.scheduleTimeout(t.timeoutPropose(r), propose, t.state.height, t.state.round) + } + + go t.processFutureMessages(t.state.height, t.state.round) +} + +//nolint:gocyclo +func (t *Tendermint[V, H, A]) processFutureMessages(h, r uint) { + t.futureMessagesMu.Lock() + defer t.futureMessagesMu.Unlock() + + proposals, prevotes, precommits := t.futureMessages.allMessages(h, r) + if len(proposals) > 0 { + for _, addrProposals := range proposals { + for _, proposal := range addrProposals { + select { + case <-t.quit: + return + case t.proposalsCh <- proposal: + } + } + } + } + + if len(prevotes) > 0 { + for _, addrPrevotes := range prevotes { + for _, vote := range addrPrevotes { + select { + case <-t.quit: + return + case t.prevotesCh <- vote: + } + } + } + } + + if len(precommits) > 0 { + for _, addrPrecommits := range precommits { + for _, vote := range addrPrecommits { + select { + case <-t.quit: + return + case t.precommitsCh <- vote: + + } + } + } + } + + t.futureMessages.deleteRoundMessages(h, r) +} + +type timeout struct { + *time.Timer + + s step + h uint + r uint +} + +func (t *Tendermint[V, H, A]) scheduleTimeout(duration time.Duration, s step, h, r uint) { + tm := timeout{s: s, h: h, r: r} + tm.Timer = time.AfterFunc(duration, func() { + select { + case <-t.quit: + case t.timeoutsCh <- tm: + } + }) + t.scheduledTms = append(t.scheduledTms, tm) +} + +func (t *Tendermint[_, H, A]) OnTimeoutPropose(h, r uint) { + if t.state.height == h && t.state.round == r && t.state.step == propose { + vote := Prevote[H, A]{ + Vote: Vote[H, A]{ + Height: t.state.height, + Round: t.state.round, + ID: nil, + Sender: t.nodeAddr, + }, + } + t.messages.addPrevote(vote) + fmt.Println("About to send prevot nil") + t.broadcasters.PrevoteBroadcaster.Broadcast(vote) + t.state.step = prevote + fmt.Println("Set the step to prevote") + } +} + +func (t *Tendermint[_, H, A]) OnTimeoutPrevote(h, r uint) { + if t.state.height == h && t.state.round == r && t.state.step == prevote { + vote := Precommit[H, A]{ + Vote: Vote[H, A]{ + Height: t.state.height, + Round: t.state.round, + ID: nil, + Sender: t.nodeAddr, + }, + } + t.messages.addPrecommit(vote) + fmt.Println("About to send precommit nil") + t.broadcasters.PrecommitBroadcaster.Broadcast(vote) + t.state.step = precommit + fmt.Println("Set the step to precommit") + } +} + +func (t *Tendermint[_, _, _]) OnTimeoutPrecommit(h, r uint) { + if t.state.height == h && t.state.round == r { + t.startRound(r + 1) + } +} + +// line55 assumes the caller has acquired a mutex for accessing future messages. +func (t *Tendermint[V, H, A]) line55(futureR uint) { + vals := make(map[A]struct{}) + proposals, prevotes, precommits := t.futureMessages.allMessages(t.state.height, futureR) + + // If a validator has sent proposl, prevote and precommit from a future round then it will only be counted once. + for addr := range proposals { + vals[addr] = struct{}{} + } + + for addr := range prevotes { + vals[addr] = struct{}{} + } + + for addr := range precommits { + vals[addr] = struct{}{} + } + + if t.validatorSetVotingPower(slices.Collect(maps.Keys(vals))) > f(t.validators.TotalVotingPower(t.state.height)) { + t.startRound(futureR) + } +} + +func (t *Tendermint[V, H, A]) validatorSetVotingPower(vals []A) uint { + var totalVotingPower uint + for _, v := range vals { + totalVotingPower += t.validators.ValidatorVotingPower(v) + } + return totalVotingPower +} + +// Todo: add separate unit tests to check f and q thresholds. +func f(totalVotingPower uint) uint { + // note: integer division automatically floors the result as it return the quotient. + return (totalVotingPower - 1) / 3 +} + +func q(totalVotingPower uint) uint { + // Unfortunately there is no ceiling function for integers in go. + d := totalVotingPower * 2 + q := d / 3 + r := d % 3 + if r > 0 { + q++ + } + return q +} diff --git a/consensus/tendermint/tendermint_test.go b/consensus/tendermint/tendermint_test.go new file mode 100644 index 0000000000..f5c53f5600 --- /dev/null +++ b/consensus/tendermint/tendermint_test.go @@ -0,0 +1,282 @@ +package tendermint + +import ( + "fmt" + "testing" + "time" + + "github.com/NethermindEth/juno/core/felt" + "github.com/NethermindEth/juno/utils" + "github.com/stretchr/testify/assert" +) + +// Implements Hashable interface +type value uint64 + +func (t value) Hash() felt.Felt { + return *new(felt.Felt).SetUint64(uint64(t)) +} + +// Implements Application[value, felt.Felt] interface +type app struct { + cur value +} + +func newApp() *app { return &app{} } + +func (a *app) Value() value { + a.cur = (a.cur + 1) % 100 + return a.cur +} + +func (a *app) Valid(v value) bool { + return v < 100 +} + +// Implements Blockchain[value, felt.Felt] interface +type chain struct { + curHeight uint + decision map[uint]value + decisionCertificates map[uint][]Precommit[felt.Felt, felt.Felt] +} + +func newChain() *chain { + return &chain{ + decision: make(map[uint]value), + decisionCertificates: make(map[uint][]Precommit[felt.Felt, felt.Felt]), + } +} + +func (c *chain) Height() uint { + return c.curHeight +} + +func (c *chain) Commit(h uint, v value, precommits []Precommit[felt.Felt, felt.Felt]) { + c.decision[c.curHeight] = v + c.decisionCertificates[c.curHeight] = precommits + c.curHeight++ +} + +// Implements Validators[felt.Felt] interface +type validators struct { + totalVotingPower uint + vals []felt.Felt +} + +func newVals() *validators { return &validators{} } + +func (v *validators) TotalVotingPower(h uint) uint { + return v.totalVotingPower +} + +func (v *validators) ValidatorVotingPower(validatorAddr felt.Felt) uint { + return 1 +} + +// Proposer is implements round robin +func (v *validators) Proposer(h, r uint) felt.Felt { + i := (h + r) % v.totalVotingPower + return v.vals[i] +} + +func (v *validators) addValidator(addr felt.Felt) { + v.vals = append(v.vals, addr) + v.totalVotingPower++ +} + +// Implements Listener[M Message[V, H], V Hashable[H], H Hash] and Broadcasters[V Hashable[H], H Hash, A Addr] interface +type senderAndReceiver[M Message[V, H, A], V Hashable[H], H Hash, A Addr] struct { + mCh chan M +} + +func (r *senderAndReceiver[M, _, _, _]) send(m M) { + r.mCh <- m +} + +func (r *senderAndReceiver[M, _, _, _]) Listen() <-chan M { + return r.mCh +} + +func (r *senderAndReceiver[M, _, _, _]) Broadcast(msg M) { r.mCh <- msg } + +func (r *senderAndReceiver[M, _, _, A]) SendMsg(validatorAddr A, msg M) {} + +func newSenderAndReceiver[M Message[V, H, A], V Hashable[H], H Hash, A Addr]() *senderAndReceiver[M, V, H, A] { + return &senderAndReceiver[M, V, H, A]{mCh: make(chan M, 10)} +} + +func testListenersAndBroadcasters() (Listeners[value, felt.Felt, felt.Felt], Broadcasters[value, + felt.Felt, felt.Felt], +) { + listeners := Listeners[value, felt.Felt, felt.Felt]{ + ProposalListener: newSenderAndReceiver[Proposal[value, felt.Felt, felt.Felt], value, felt.Felt, felt.Felt](), + PrevoteListener: newSenderAndReceiver[Prevote[felt.Felt, felt.Felt], value, felt.Felt, felt.Felt](), + PrecommitListener: newSenderAndReceiver[Precommit[felt.Felt, felt.Felt], value, felt.Felt, felt.Felt](), + } + + broadcasters := Broadcasters[value, felt.Felt, felt.Felt]{ + ProposalBroadcaster: newSenderAndReceiver[Proposal[value, felt.Felt, felt.Felt], value, felt.Felt, felt.Felt](), + PrevoteBroadcaster: newSenderAndReceiver[Prevote[felt.Felt, felt.Felt], value, felt.Felt, felt.Felt](), + PrecommitBroadcaster: newSenderAndReceiver[Precommit[felt.Felt, felt.Felt], value, felt.Felt, felt.Felt](), + } + + return listeners, broadcasters +} + +func TestStartRound(t *testing.T) { + nodeAddr := new(felt.Felt).SetBytes([]byte("my node address")) + val2, val3, val4 := new(felt.Felt).SetUint64(2), new(felt.Felt).SetUint64(3), new(felt.Felt).SetUint64(4) + + tm := func(r uint) time.Duration { return time.Duration(r) * time.Nanosecond } + + t.Run("node is the proposer", func(t *testing.T) { + listeners, broadcasters := testListenersAndBroadcasters() + app, chain, vals := newApp(), newChain(), newVals() + + vals.addValidator(*nodeAddr) + vals.addValidator(*val2) + vals.addValidator(*val3) + vals.addValidator(*val4) + + algo := New[value, felt.Felt, felt.Felt](*nodeAddr, app, chain, vals, listeners, broadcasters, tm, tm, tm) + + expectedHeight, expectedRound := uint(0), uint(0) + expectedProposalMsg := Proposal[value, felt.Felt, felt.Felt]{ + Height: 0, + Round: 0, + ValidRound: nil, + Value: utils.Ptr(app.cur + 1), + Sender: *nodeAddr, + } + + proposalBroadcaster := broadcasters.ProposalBroadcaster.(*senderAndReceiver[Proposal[value, felt.Felt, + felt.Felt], value, felt.Felt, felt.Felt]) + + algo.Start() + algo.Stop() + + proposal := <-proposalBroadcaster.mCh + + assert.Equal(t, expectedProposalMsg, proposal) + assert.Equal(t, 1, len(algo.messages.proposals[expectedHeight][expectedRound][*nodeAddr])) + assert.Equal(t, expectedProposalMsg, algo.messages.proposals[expectedHeight][expectedRound][*nodeAddr][0]) + + assert.Equal(t, propose, algo.state.step) + assert.Equal(t, expectedHeight, algo.state.height) + assert.Equal(t, expectedRound, algo.state.round) + }) + + t.Run("node is not the proposer: schedule timeoutPropose", func(t *testing.T) { + listeners, broadcasters := testListenersAndBroadcasters() + app, chain, vals := newApp(), newChain(), newVals() + + vals.addValidator(*val2) + vals.addValidator(*val3) + vals.addValidator(*val4) + vals.addValidator(*nodeAddr) + + algo := New[value, felt.Felt, felt.Felt](*nodeAddr, app, chain, vals, listeners, broadcasters, tm, tm, tm) + + algo.Start() + algo.Stop() + + assert.Equal(t, 1, len(algo.scheduledTms)) + scheduledTm := algo.scheduledTms[0] + + assert.Equal(t, propose, scheduledTm.s) + assert.Equal(t, uint(0), scheduledTm.h) + assert.Equal(t, uint(0), scheduledTm.r) + + assert.Equal(t, propose, algo.state.step) + assert.Equal(t, uint(0), algo.state.height) + assert.Equal(t, uint(0), algo.state.round) + }) + + t.Run("OnTimeoutPropose: round zero the node is not the proposer thus send a prevote nil", func(t *testing.T) { + listeners, broadcasters := testListenersAndBroadcasters() + app, chain, vals := newApp(), newChain(), newVals() + // The algo needs to run for a minimum amount of time but it cannot be long enough the state to change + // multiple times. This can happen if the timeouts are too small. + tm := func(r uint) time.Duration { + if r == 0 { + fmt.Println("R is ", r) + return time.Nanosecond + } + return time.Second + } + + vals.addValidator(*val2) + vals.addValidator(*val3) + vals.addValidator(*val4) + vals.addValidator(*nodeAddr) + + algo := New[value, felt.Felt, felt.Felt](*nodeAddr, app, chain, vals, listeners, broadcasters, tm, tm, tm) + + expectedHeight, expectedRound := uint(0), uint(0) + expectedPrevoteMsg := Prevote[felt.Felt, felt.Felt]{ + Vote: Vote[felt.Felt, felt.Felt]{ + Height: 0, + Round: 0, + ID: nil, + Sender: *nodeAddr, + }, + } + + prevoteBroadcaster := broadcasters.PrevoteBroadcaster.(*senderAndReceiver[Prevote[felt.Felt, felt.Felt], value, + felt.Felt, felt.Felt]) + + algo.Start() + time.Sleep(time.Millisecond) + algo.Stop() + + prevoteMsg := <-prevoteBroadcaster.mCh + + assert.Equal(t, expectedPrevoteMsg, prevoteMsg) + assert.Equal(t, 1, len(algo.messages.prevotes[expectedHeight][expectedRound][*nodeAddr])) + assert.Equal(t, expectedPrevoteMsg, algo.messages.prevotes[expectedHeight][expectedRound][*nodeAddr][0]) + + assert.Equal(t, prevote, algo.state.step) + assert.Equal(t, expectedHeight, algo.state.height) + assert.Equal(t, expectedRound, algo.state.round) + }) +} + +func TestThresholds(t *testing.T) { + tests := []struct { + n uint + q uint + f uint + }{ + {1, 1, 0}, + {2, 2, 0}, + {3, 2, 0}, + {4, 3, 1}, + {5, 4, 1}, + {6, 4, 1}, + {7, 5, 2}, + {11, 8, 3}, + {15, 10, 4}, + {20, 14, 6}, + {100, 67, 33}, + {150, 100, 49}, + {2000, 1334, 666}, + {2509, 1673, 836}, + {3045, 2030, 1014}, + {7689, 5126, 2562}, + {10032, 6688, 3343}, + {12932, 8622, 4310}, + {15982, 10655, 5327}, + {301234, 200823, 100411}, + {301235, 200824, 100411}, + {301236, 200824, 100411}, + } + + for _, test := range tests { + assert.Equal(t, test.q, q(test.n)) + assert.Equal(t, test.f, f(test.n)) + + } +} + +// Todo: Add tests for round change where existing messages are processed +// Todo: Add malicious test