Skip to content

Commit

Permalink
feat: add cluster tests (#14)
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

* Add initial cluster tests, simplify state

* Add coverage for message equals

* Fix race conditions
  • Loading branch information
zivkovicmilos authored Mar 25, 2024
1 parent c5b94aa commit 82a7687
Show file tree
Hide file tree
Showing 9 changed files with 1,329 additions and 78 deletions.
424 changes: 424 additions & 0 deletions core/cluster_test.go

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions core/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (t *Tendermint) AddProposalMessage(message *types.ProposalMessage) error {
func (t *Tendermint) AddPrevoteMessage(message *types.PrevoteMessage) error {
// Verify the incoming message
if err := t.verifyMessage(message); err != nil {
return fmt.Errorf("unable to verify proposal message, %w", err)
return fmt.Errorf("unable to verify prevote message, %w", err)
}

// Add the message to the store
Expand All @@ -44,7 +44,7 @@ func (t *Tendermint) AddPrevoteMessage(message *types.PrevoteMessage) error {
func (t *Tendermint) AddPrecommitMessage(message *types.PrecommitMessage) error {
// Verify the incoming message
if err := t.verifyMessage(message); err != nil {
return fmt.Errorf("unable to verify proposal message, %w", err)
return fmt.Errorf("unable to verify precommit message, %w", err)
}

// Add the message to the store
Expand Down
205 changes: 204 additions & 1 deletion core/mocks_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package core

import "github.com/gnolang/go-tendermint/messages/types"
import (
"context"
"sync"
"sync/atomic"

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

type (
broadcastProposeDelegate func(*types.ProposalMessage)
Expand Down Expand Up @@ -205,3 +211,200 @@ func (m *mockMessage) Verify() error {

return nil
}

// mockNodeContext keeps track of the node runtime context
type mockNodeContext struct {
ctx context.Context
cancelFn context.CancelFunc
}

// mockNodeWg is the WaitGroup wrapper for the cluster nodes
type mockNodeWg struct {
sync.WaitGroup
count int64
}

func (wg *mockNodeWg) Add(delta int) {
wg.WaitGroup.Add(delta)
}

func (wg *mockNodeWg) Done() {
wg.WaitGroup.Done()
atomic.AddInt64(&wg.count, 1)
}

func (wg *mockNodeWg) resetDone() {
atomic.StoreInt64(&wg.count, 0)
}

type (
verifierConfigCallback func(*mockVerifier)
nodeConfigCallback func(*mockNode)
broadcastConfigCallback func(*mockBroadcast)
signerConfigCallback func(*mockSigner)
)

// mockCluster represents a mock Tendermint cluster
type mockCluster struct {
nodes []*Tendermint // references to the nodes in the cluster
ctxs []mockNodeContext // context handlers for the nodes in the cluster
finalizedProposals [][]byte // finalized proposals for the nodes

stoppedWg mockNodeWg
}

// newMockCluster creates a new mock Tendermint cluster
func newMockCluster(
count uint64,
verifierCallbackMap map[int]verifierConfigCallback,
nodeCallbackMap map[int]nodeConfigCallback,
broadcastCallbackMap map[int]broadcastConfigCallback,
signerCallbackMap map[int]signerConfigCallback,
optionsMap map[int][]Option,
) *mockCluster {
if count < 1 {
return nil
}

nodes := make([]*Tendermint, count)
nodeCtxs := make([]mockNodeContext, count)

for index := 0; index < int(count); index++ {
var (
verifier = &mockVerifier{}
node = &mockNode{}
broadcast = &mockBroadcast{}
signer = &mockSigner{}
options = make([]Option, 0)
)

// Execute set callbacks, if any
if verifierCallbackMap != nil {
if verifierCallback, isSet := verifierCallbackMap[index]; isSet {
verifierCallback(verifier)
}
}

if nodeCallbackMap != nil {
if nodeCallback, isSet := nodeCallbackMap[index]; isSet {
nodeCallback(node)
}
}

if broadcastCallbackMap != nil {
if broadcastCallback, isSet := broadcastCallbackMap[index]; isSet {
broadcastCallback(broadcast)
}
}

if signerCallbackMap != nil {
if signerCallback, isSet := signerCallbackMap[index]; isSet {
signerCallback(signer)
}
}

if optionsMap != nil {
if opts, isSet := optionsMap[index]; isSet {
options = opts
}
}

// Create a new instance of the Tendermint node
nodes[index] = NewTendermint(
verifier,
node,
broadcast,
signer,
options...,
)

// Instantiate context for the nodes
ctx, cancelFn := context.WithCancel(context.Background())
nodeCtxs[index] = mockNodeContext{
ctx: ctx,
cancelFn: cancelFn,
}
}

return &mockCluster{
nodes: nodes,
ctxs: nodeCtxs,
finalizedProposals: make([][]byte, count),
}
}

// runSequence runs the cluster sequence for the given height
func (m *mockCluster) runSequence(height uint64) {
m.stoppedWg.resetDone()

for nodeIndex, node := range m.nodes {
m.stoppedWg.Add(1)

go func(
ctx context.Context,
node *Tendermint,
nodeIndex int,
height uint64,
) {
defer m.stoppedWg.Done()

// Start the main run loop for the node
finalizedProposal := node.RunSequence(ctx, height)

m.finalizedProposals[nodeIndex] = finalizedProposal
}(m.ctxs[nodeIndex].ctx, node, nodeIndex, height)
}
}

// awaitCompletion waits for completion of all
// nodes in the cluster
func (m *mockCluster) awaitCompletion() {
// Wait for all main run loops to signalize
// that they're finished
m.stoppedWg.Wait()
}

// pushProposalMessage relays the proposal message to all nodes in the cluster
func (m *mockCluster) pushProposalMessage(message *types.ProposalMessage) error {
for _, node := range m.nodes {
if err := node.AddProposalMessage(message); err != nil {
return err
}
}

return nil
}

// pushPrevoteMessage relays the prevote message to all nodes in the cluster
func (m *mockCluster) pushPrevoteMessage(message *types.PrevoteMessage) error {
for _, node := range m.nodes {
if err := node.AddPrevoteMessage(message); err != nil {
return err
}
}

return nil
}

// pushPrecommitMessage relays the precommit message to all nodes in the cluster
func (m *mockCluster) pushPrecommitMessage(message *types.PrecommitMessage) error {
for _, node := range m.nodes {
if err := node.AddPrecommitMessage(message); err != nil {
return err
}
}

return nil
}

// areAllNodesOnRound checks to make sure all nodes
// are on the same specified round
func (m *mockCluster) areAllNodesOnRound(round uint64) bool {
for _, node := range m.nodes {
if node.state.getRound() != round {
return false
}
}

return true
}
3 changes: 0 additions & 3 deletions core/quorum_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package core
import (
"testing"

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

Expand Down Expand Up @@ -97,7 +96,6 @@ func TestTendermint_QuorumSuperMajority(t *testing.T) {
nil,
nil,
)
tm.state = newState(&types.View{})

convertedMessages := make([]Message, 0, len(testCase.messages))

Expand Down Expand Up @@ -204,7 +202,6 @@ func TestTendermint_QuorumFaultyMajority(t *testing.T) {
nil,
nil,
)
tm.state = newState(&types.View{})

convertedMessages := make([]Message, 0, len(testCase.messages))

Expand Down
14 changes: 11 additions & 3 deletions core/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (s *step) get() step {
type state struct {
view *types.View

acceptedProposal *types.ProposalMessage // TODO make this a []byte
acceptedProposal []byte
acceptedProposalID []byte

lockedValue []byte
Expand All @@ -42,9 +42,12 @@ type state struct {
}

// newState creates a fresh state using the given view
func newState(view *types.View) state {
func newState() state {
return state{
view: view,
view: &types.View{
Height: 0, // zero height
Round: 0, // zero round
},
step: propose,
acceptedProposal: nil,
acceptedProposalID: nil,
Expand Down Expand Up @@ -74,3 +77,8 @@ func (s *state) increaseRound() {
func (s *state) setRound(r uint64) {
atomic.SwapUint64(&s.view.Round, r)
}

// setHeight sets the current view height to the given value [THREAD SAFE]
func (s *state) setHeight(h uint64) {
atomic.SwapUint64(&s.view.Height, h)
}
Loading

0 comments on commit 82a7687

Please sign in to comment.