Skip to content

Commit

Permalink
feat: add state tests (#8)
Browse files Browse the repository at this point in the history
* Add proposer test case

* Add proposer test case for utilizing old proposal

* Simplify view comparison in tests

* Add runPropose verification

* Add runPrevote verification

* Add prevote step timeout

* Add runPrecommit verification

* Update watchForRoundJumps

* Add missing cleanup, broadcast

* Clean up the consensus state

* Remove leftover comment

* Simplify the final timer expiring

* Add additional comments to the types

* Add support for options

* Fix uninitialized logger in tests

* Add unit test for non-proposer receiving a fresh proposal

* Swap out the useless bloom

* Add unit tests for non-proposer receiving invalid proposals

* Add unit tests for runPrevote

* Add unit tests for runPrecommit

* Add unit tests for the cache, options, quorum

* Simplify state timeouts

* Add unit tests for future round jumps

* Add unit tests for dropping messages

* Add unit tests for dropping messages
  • Loading branch information
zivkovicmilos authored Mar 21, 2024
1 parent a0c948d commit c5b94aa
Show file tree
Hide file tree
Showing 16 changed files with 2,447 additions and 291 deletions.
9 changes: 4 additions & 5 deletions core/broadcast.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import (
)

// buildProposalMessage builds a proposal message using the given proposal
func (t *Tendermint) buildProposalMessage(proposal []byte) *types.ProposalMessage {
func (t *Tendermint) buildProposalMessage(proposal []byte, proposalRound int64) *types.ProposalMessage {
var (
height = t.state.getHeight()
round = t.state.getRound()
validRound = t.state.validRound
height = t.state.getHeight()
round = t.state.getRound()
)

// Build the proposal message (assumes the node will sign it)
Expand All @@ -20,7 +19,7 @@ func (t *Tendermint) buildProposalMessage(proposal []byte) *types.ProposalMessag
},
Sender: t.node.ID(),
Proposal: proposal,
ProposalRound: validRound,
ProposalRound: proposalRound,
}

// Sign the message
Expand Down
2 changes: 0 additions & 2 deletions core/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ type msgType interface {
*types.ProposalMessage | *types.PrevoteMessage | *types.PrecommitMessage
}

// TODO @petar-dambovaliev, how we can make this less ugly?
type cacheMessage interface {
msgType

Message
}

Expand Down
70 changes: 70 additions & 0 deletions core/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package core

import (
"testing"

"github.com/gnolang/go-tendermint/messages/types"
"github.com/stretchr/testify/assert"
)

func TestMessageCache_AddMessages(t *testing.T) {
t.Parallel()

isValidFn := func(_ *types.PrevoteMessage) bool {
return true
}

t.Run("non-duplicate messages", func(t *testing.T) {
t.Parallel()

// Create the cache
cache := newMessageCache[*types.PrevoteMessage](isValidFn)

// Generate non-duplicate messages
messages := generatePrevoteMessages(t, 10, &types.View{}, nil)

// Add the messages
cache.addMessages(messages)

// Make sure all messages are added
fetchedMessages := cache.getMessages()

for index, message := range messages {
assert.True(t, message.Equals(fetchedMessages[index]))
}
})

t.Run("duplicate messages", func(t *testing.T) {
t.Parallel()

var (
numMessages = 10
numDuplicates = numMessages / 2
)

// Create the cache
cache := newMessageCache[*types.PrevoteMessage](isValidFn)

// Generate non-duplicate messages
messages := generatePrevoteMessages(t, numMessages, &types.View{}, nil)

// Make sure some are duplicated
for i := 0; i < numDuplicates; i++ {
messages[i].Sender = []byte("common sender")
}

expectedMessages := messages[numDuplicates-1:]

// Add the messages
cache.addMessages(messages)

// Make sure all messages are added
fetchedMessages := cache.getMessages()

assert.Len(t, fetchedMessages, len(expectedMessages))

for index, message := range expectedMessages {
assert.True(t, message.Equals(fetchedMessages[index]))
}
})
}
66 changes: 61 additions & 5 deletions core/mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ package core
import "github.com/gnolang/go-tendermint/messages/types"

type (
broadcastProposalDelegate func(*types.ProposalMessage)
broadcastProposeDelegate func(*types.ProposalMessage)
broadcastPrevoteDelegate func(*types.PrevoteMessage)
broadcastPrecommitDelegate func(*types.PrecommitMessage)
)

type mockBroadcast struct {
broadcastProposalFn broadcastProposalDelegate
broadcastProposeFn broadcastProposeDelegate
broadcastPrevoteFn broadcastPrevoteDelegate
broadcastPrecommitFn broadcastPrecommitDelegate
}

func (m *mockBroadcast) BroadcastProposal(message *types.ProposalMessage) {
if m.broadcastProposalFn != nil {
m.broadcastProposalFn(message)
func (m *mockBroadcast) BroadcastPropose(message *types.ProposalMessage) {
if m.broadcastProposeFn != nil {
m.broadcastProposeFn(message)
}
}

Expand Down Expand Up @@ -149,3 +149,59 @@ func (m *mockVerifier) IsValidProposal(proposal []byte, height uint64) bool {

return false
}

type (
getViewDelegate func() *types.View
getSenderDelegate func() []byte
getSignatureDelegate func() []byte
getSignaturePayloadDelegate func() []byte
verifyDelegate func() error
)

type mockMessage struct {
getViewFn getViewDelegate
getSenderFn getSenderDelegate
getSignatureFn getSignatureDelegate
getSignaturePayloadFn getSignaturePayloadDelegate
verifyFn verifyDelegate
}

func (m *mockMessage) GetView() *types.View {
if m.getViewFn != nil {
return m.getViewFn()
}

return nil
}

func (m *mockMessage) GetSender() []byte {
if m.getSenderFn != nil {
return m.getSenderFn()
}

return nil
}

func (m *mockMessage) GetSignature() []byte {
if m.getSignatureFn != nil {
return m.getSignatureFn()
}

return nil
}

func (m *mockMessage) GetSignaturePayload() []byte {
if m.getSignaturePayloadFn != nil {
return m.getSignaturePayloadFn()
}

return nil
}

func (m *mockMessage) Verify() error {
if m.verifyFn != nil {
return m.verifyFn()
}

return nil
}
87 changes: 87 additions & 0 deletions core/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package core

import (
"io"
"log/slog"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestNewTendermint_Options(t *testing.T) {
t.Parallel()

t.Run("Withlogger", func(t *testing.T) {
t.Parallel()

l := slog.New(slog.NewTextHandler(io.Discard, nil))

tm := NewTendermint(
nil,
nil,
nil,
nil,
WithLogger(l),
)

assert.Equal(t, tm.logger, l)
})

t.Run("WithProposeTimeout", func(t *testing.T) {
t.Parallel()

timeout := Timeout{
Initial: 500 * time.Millisecond,
Delta: 0,
}

tm := NewTendermint(
nil,
nil,
nil,
nil,
WithProposeTimeout(timeout),
)

assert.Equal(t, tm.timeouts[propose], timeout)
})

t.Run("WithPrevoteTimeout", func(t *testing.T) {
t.Parallel()

timeout := Timeout{
Initial: 500 * time.Millisecond,
Delta: 0,
}

tm := NewTendermint(
nil,
nil,
nil,
nil,
WithPrevoteTimeout(timeout),
)

assert.Equal(t, tm.timeouts[prevote], timeout)
})

t.Run("WithPrecommitTimeout", func(t *testing.T) {
t.Parallel()

timeout := Timeout{
Initial: 500 * time.Millisecond,
Delta: 0,
}

tm := NewTendermint(
nil,
nil,
nil,
nil,
WithPrecommitTimeout(timeout),
)

assert.Equal(t, tm.timeouts[precommit], timeout)
})
}
14 changes: 10 additions & 4 deletions core/quorum.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@ package core

// hasSuperMajority verifies that there is a 2F+1 voting power majority
// in the given message set.
// This follows the constraint that N > 3f, i.e., the total voting power of faulty processes is smaller than
// This follows the constraint that N > 3F, i.e., the total voting power of faulty processes is smaller than
// one third of the total voting power
func (t *Tendermint) hasSuperMajority(messages []Message) bool {
return t.verifier.GetSumVotingPower(messages) > (2 * t.verifier.GetTotalVotingPower(t.state.getHeight()) / 3)
sumVotingPower := t.verifier.GetSumVotingPower(messages)
totalVotingPower := t.verifier.GetTotalVotingPower(t.state.getHeight())

return sumVotingPower > (2 * totalVotingPower / 3)
}

// hasFaultyMajority verifies that there is an F+1 voting power majority
// in the given message set.
// This follows the constraint that N > 3f, i.e., the total voting power of faulty processes is smaller than
// This follows the constraint that N > 3F, i.e., the total voting power of faulty processes is smaller than
// one third of the total voting power
func (t *Tendermint) hasFaultyMajority(messages []Message) bool {
return t.verifier.GetSumVotingPower(messages) > (t.verifier.GetTotalVotingPower(t.state.getHeight()) / 3)
sumVotingPower := t.verifier.GetSumVotingPower(messages)
totalVotingPower := t.verifier.GetTotalVotingPower(t.state.getHeight())

return sumVotingPower > totalVotingPower/3
}
Loading

0 comments on commit c5b94aa

Please sign in to comment.