// Copyright 2018 The dexon-consensus Authors
// This file is part of the dexon-consensus library.
//
// The dexon-consensus library is free software: you can redistribute it
// and/or modify it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
//
// The dexon-consensus library is distributed in the hope that it will be
// useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the dexon-consensus library. If not, see
// <http://www.gnu.org/licenses/>.
package test
import (
"sort"
"testing"
"time"
"github.com/dexon-foundation/dexon-consensus/common"
"github.com/dexon-foundation/dexon-consensus/core/crypto"
"github.com/dexon-foundation/dexon-consensus/core/crypto/dkg"
"github.com/dexon-foundation/dexon-consensus/core/crypto/ecdsa"
"github.com/dexon-foundation/dexon-consensus/core/types"
typesDKG "github.com/dexon-foundation/dexon-consensus/core/types/dkg"
"github.com/dexon-foundation/dexon-consensus/core/utils"
"github.com/stretchr/testify/suite"
)
type StateTestSuite struct {
suite.Suite
}
func (s *StateTestSuite) newDKGMasterPublicKey(
round uint64, reset uint64) *typesDKG.MasterPublicKey {
prvKey, err := ecdsa.NewPrivateKey()
s.Require().NoError(err)
pubKey := prvKey.PublicKey()
nodeID := types.NewNodeID(pubKey)
_, pubShare := dkg.NewPrivateKeyShares(3)
dID, err := dkg.BytesID(nodeID.Hash[:])
s.Require().NoError(err)
return &typesDKG.MasterPublicKey{
ProposerID: nodeID,
Round: round,
Reset: reset,
DKGID: dID,
PublicKeyShares: *pubShare.Move(),
}
}
func (s *StateTestSuite) newDKGComplaint(
round uint64, reset uint64) *typesDKG.Complaint {
prvKey, err := ecdsa.NewPrivateKey()
s.Require().NoError(err)
nodeID := types.NewNodeID(prvKey.PublicKey())
comp := &typesDKG.Complaint{
Round: round,
Reset: reset,
PrivateShare: typesDKG.PrivateShare{
ProposerID: nodeID,
ReceiverID: nodeID,
Round: round,
Reset: reset,
PrivateShare: *dkg.NewPrivateKey(),
},
}
signer := utils.NewSigner(prvKey)
s.Require().NoError(signer.SignDKGComplaint(comp))
s.Require().NoError(signer.SignDKGPrivateShare(&comp.PrivateShare))
s.Require().False(comp.IsNack())
return comp
}
func (s *StateTestSuite) newDKGMPKReady(
round uint64, reset uint64) *typesDKG.MPKReady {
prvKey, err := ecdsa.NewPrivateKey()
s.Require().NoError(err)
ready := &typesDKG.MPKReady{Round: round, Reset: reset}
s.Require().NoError(utils.NewSigner(prvKey).SignDKGMPKReady(ready))
return ready
}
func (s *StateTestSuite) newDKGFinal(
round uint64, reset uint64) *typesDKG.Finalize {
prvKey, err := ecdsa.NewPrivateKey()
s.Require().NoError(err)
final := &typesDKG.Finalize{Round: round, Reset: reset}
s.Require().NoError(utils.NewSigner(prvKey).SignDKGFinalize(final))
return final
}
func (s *StateTestSuite) compareNodes(node1, node2 []crypto.PublicKey) bool {
id1 := common.Hashes{}
for _, n := range node1 {
id1 = append(id1, types.NewNodeID(n).Hash)
}
sort.Sort(id1)
id2 := common.Hashes{}
for _, n := range node2 {
id2 = append(id2, types.NewNodeID(n).Hash)
}
sort.Sort(id2)
if len(id1) != len(id2) {
return false
}
for idx, id := range id1 {
if id != id2[idx] {
return false
}
}
return true
}
func (s *StateTestSuite) findNode(
nodes []crypto.PublicKey, node crypto.PublicKey) bool {
nodeID := types.NewNodeID(node)
for _, n := range nodes {
nID := types.NewNodeID(n)
if nID == nodeID {
return true
}
}
return false
}
func (s *StateTestSuite) makeDKGChanges(
st *State,
masterPubKey *typesDKG.MasterPublicKey,
ready *typesDKG.MPKReady,
complaint *typesDKG.Complaint,
final *typesDKG.Finalize) {
s.Require().NoError(st.RequestChange(StateAddDKGMasterPublicKey,
masterPubKey))
s.Require().NoError(st.RequestChange(StateAddDKGMPKReady, ready))
s.Require().NoError(st.RequestChange(StateAddDKGComplaint, complaint))
s.Require().NoError(st.RequestChange(StateAddDKGFinal, final))
}
func (s *StateTestSuite) makeConfigChanges(st *State) {
st.RequestChange(StateChangeLambdaBA, time.Nanosecond)
st.RequestChange(StateChangeLambdaDKG, time.Millisecond)
st.RequestChange(StateChangeRoundLength, uint64(1001))
st.RequestChange(StateChangeMinBlockInterval, time.Second)
st.RequestChange(StateChangeNotarySetSize, uint32(5))
}
func (s *StateTestSuite) checkConfigChanges(config *types.Config) {
req := s.Require()
req.Equal(config.LambdaBA, time.Nanosecond)
req.Equal(config.LambdaDKG, time.Millisecond)
req.Equal(config.RoundLength, uint64(1001))
req.Equal(config.MinBlockInterval, time.Second)
req.Equal(config.NotarySetSize, uint32(5))
}
func (s *StateTestSuite) TestEqual() {
var (
req = s.Require()
lambda = 250 * time.Millisecond
)
_, genesisNodes, err := NewKeys(20)
req.NoError(err)
st := NewState(1, genesisNodes, lambda, &common.NullLogger{}, true)
req.NoError(st.Equal(st))
// One node is missing.
st1 := NewState(1, genesisNodes, lambda, &common.NullLogger{}, true)
for nID := range st1.nodes {
delete(st1.nodes, nID)
break
}
req.Equal(st.Equal(st1), ErrStateNodeSetNotEqual)
// Make some changes.
st2 := st.Clone()
req.NoError(st.Equal(st2))
s.makeConfigChanges(st)
req.EqualError(ErrStateConfigNotEqual, st.Equal(st2).Error())
req.NoError(st.ProposeCRS(2, common.NewRandomHash()))
req.NoError(st.RequestChange(StateResetDKG, common.NewRandomHash()))
masterPubKey := s.newDKGMasterPublicKey(2, 1)
ready := s.newDKGMPKReady(2, 1)
comp := s.newDKGComplaint(2, 1)
final := s.newDKGFinal(2, 1)
s.makeDKGChanges(st, masterPubKey, ready, comp, final)
// Remove dkg complaints from cloned one to check if equal.
st3 := st.Clone()
req.NoError(st.Equal(st3))
delete(st3.dkgComplaints, uint64(2))
req.EqualError(ErrStateDKGComplaintsNotEqual, st.Equal(st3).Error())
// Remove dkg master public key from cloned one to check if equal.
st4 := st.Clone()
req.NoError(st.Equal(st4))
delete(st4.dkgMasterPublicKeys, uint64(2))
req.EqualError(ErrStateDKGMasterPublicKeysNotEqual, st.Equal(st4).Error())
// Remove dkg ready from cloned one to check if equal.
st4a := st.Clone()
req.NoError(st.Equal(st4a))
delete(st4a.dkgReadys, uint64(2))
req.EqualError(ErrStateDKGMPKReadysNotEqual, st.Equal(st4a).Error())
// Remove dkg finalize from cloned one to check if equal.
st5 := st.Clone()
req.NoError(st.Equal(st5))
delete(st5.dkgFinals, uint64(2))
req.EqualError(ErrStateDKGFinalsNotEqual, st.Equal(st5).Error())
// Remove dkgResetCount from cloned one to check if equal.
st6 := st.Clone()
req.NoError(st.Equal(st6))
delete(st6.dkgResetCount, uint64(2))
req.EqualError(ErrStateDKGResetCountNotEqual, st.Equal(st6).Error())
// Switch to remote mode.
st.SwitchToRemoteMode()
// Make some change.
req.NoError(st.RequestChange(StateChangeNotarySetSize, uint32(100)))
str := st.Clone()
req.NoError(st.Equal(str))
// Remove the pending change, should not be equal.
req.Len(str.ownRequests, 1)
for k := range str.ownRequests {
delete(str.ownRequests, k)
}
req.EqualError(ErrStatePendingChangesNotEqual, st.Equal(str).Error())
}
func (s *StateTestSuite) TestPendingChangesEqual() {
var (
req = s.Require()
lambda = 250 * time.Millisecond
)
// Setup a non-local mode State instance.
_, genesisNodes, err := NewKeys(20)
req.NoError(err)
st := NewState(1, genesisNodes, lambda, &common.NullLogger{}, false)
req.NoError(st.Equal(st))
// Apply some changes.
s.makeConfigChanges(st)
crs := common.NewRandomHash()
req.NoError(st.ProposeCRS(2, crs))
masterPubKey := s.newDKGMasterPublicKey(2, 0)
ready := s.newDKGMPKReady(2, 0)
comp := s.newDKGComplaint(2, 0)
final := s.newDKGFinal(2, 0)
s.makeDKGChanges(st, masterPubKey, ready, comp, final)
}
func (s *StateTestSuite) TestLocalMode() {
// Test State with local mode.
var (
req = s.Require()
lambda = 250 * time.Millisecond
)
_, genesisNodes, err := NewKeys(20)
req.NoError(err)
st := NewState(1, genesisNodes, lambda, &common.NullLogger{}, true)
config1, nodes1 := st.Snapshot()
req.True(s.compareNodes(genesisNodes, nodes1))
// Check settings of config1 affected by genesisNodes and lambda.
req.Equal(config1.LambdaBA, lambda)
req.Equal(config1.LambdaDKG, lambda*10)
req.Equal(config1.RoundLength, uint64(1000))
req.Equal(config1.NotarySetSize, uint32(len(genesisNodes)))
// Request some changes, every fields for config should be affected.
s.makeConfigChanges(st)
// Add new node.
prvKey, err := ecdsa.NewPrivateKey()
req.NoError(err)
pubKey := prvKey.PublicKey()
st.RequestChange(StateAddNode, pubKey)
config2, newNodes := st.Snapshot()
// Check if config changes are applied.
s.checkConfigChanges(config2)
// Check if new node is added.
req.True(s.findNode(newNodes, pubKey))
// Test adding CRS.
crs := common.NewRandomHash()
req.NoError(st.ProposeCRS(2, crs))
req.Equal(st.CRS(2), crs)
// Test adding node set, DKG complaints, final, master public key.
// Make sure everything is empty before changed.
req.Empty(st.DKGMasterPublicKeys(2))
req.False(st.IsDKGMPKReady(2, 1))
req.Empty(st.DKGComplaints(2))
req.False(st.IsDKGFinal(2, 1))
// Add DKG stuffs.
masterPubKey := s.newDKGMasterPublicKey(2, 0)
ready := s.newDKGMPKReady(2, 0)
comp := s.newDKGComplaint(2, 0)
final := s.newDKGFinal(2, 0)
s.makeDKGChanges(st, masterPubKey, ready, comp, final)
// Check DKGMasterPublicKeys.
masterKeyForRound := st.DKGMasterPublicKeys(2)
req.Len(masterKeyForRound, 1)
req.True(masterKeyForRound[0].Equal(masterPubKey))
// Check IsDKGMPKReady.
req.True(st.IsDKGMPKReady(2, 1))
// Check DKGComplaints.
compForRound := st.DKGComplaints(2)
req.Len(compForRound, 1)
req.True(compForRound[0].Equal(comp))
// Check IsDKGFinal.
req.True(st.IsDKGFinal(2, 1))
// Test ResetDKG.
crs = common.NewRandomHash()
req.NoError(st.RequestChange(StateResetDKG, crs))
req.Equal(st.CRS(2), crs)
// Make sure all DKG fields are cleared.
req.Empty(st.DKGMasterPublicKeys(2))
req.False(st.IsDKGMPKReady(2, 1))
req.Empty(st.DKGComplaints(2))
req.False(st.IsDKGFinal(2, 1))
}
func (s *StateTestSuite) TestPacking() {
// Make sure everything works when requests are packing
// and unpacked to apply.
var (
req = s.Require()
lambda = 250 * time.Millisecond
)
packAndApply := func(st *State) {
// In remote mode, we need to manually convert own requests to global ones.
_, err := st.PackOwnRequests()
req.NoError(err)
// Pack changes into bytes.
b, err := st.PackRequests()
req.NoError(err)
req.NotEmpty(b)
// Apply those bytes back.
req.NoError(st.Apply(b))
}
// Make config changes.
_, genesisNodes, err := NewKeys(20)
req.NoError(err)
st := NewState(1, genesisNodes, lambda, &common.NullLogger{}, false)
s.makeConfigChanges(st)
// Add new CRS.
crs := common.NewRandomHash()
req.NoError(st.ProposeCRS(2, crs))
// Add new node.
prvKey, err := ecdsa.NewPrivateKey()
req.NoError(err)
pubKey := prvKey.PublicKey()
st.RequestChange(StateAddNode, pubKey)
// Add DKG stuffs.
masterPubKey := s.newDKGMasterPublicKey(2, 0)
ready := s.newDKGMPKReady(2, 0)
comp := s.newDKGComplaint(2, 0)
final := s.newDKGFinal(2, 0)
s.makeDKGChanges(st, masterPubKey, ready, comp, final)
// Make sure everything is empty before changed.
req.Empty(st.DKGMasterPublicKeys(2))
req.False(st.IsDKGMPKReady(2, 1))
req.Empty(st.DKGComplaints(2))
req.False(st.IsDKGFinal(2, 1))
packAndApply(st)
// Check if configs are changed.
config, nodes := st.Snapshot()
s.checkConfigChanges(config)
// Check if CRS is added.
req.Equal(st.CRS(2), crs)
// Check if new node is added.
req.True(s.findNode(nodes, pubKey))
// Check DKGMasterPublicKeys.
masterKeyForRound := st.DKGMasterPublicKeys(2)
req.Len(masterKeyForRound, 1)
req.True(masterKeyForRound[0].Equal(masterPubKey))
// Check DKGComplaints.
compForRound := st.DKGComplaints(2)
req.Len(compForRound, 1)
req.True(compForRound[0].Equal(comp))
// Check IsDKGMPKReady.
req.True(st.IsDKGMPKReady(2, 1))
// Check IsDKGFinal.
req.True(st.IsDKGFinal(2, 1))
// Test ResetDKG.
crs = common.NewRandomHash()
req.NoError(st.RequestChange(StateResetDKG, crs))
packAndApply(st)
req.Equal(st.CRS(2), crs)
// Make sure all DKG fields are cleared.
req.Empty(st.DKGMasterPublicKeys(2))
req.False(st.IsDKGMPKReady(2, 1))
req.Empty(st.DKGComplaints(2))
req.False(st.IsDKGFinal(2, 1))
}
func (s *StateTestSuite) TestRequestBroadcastAndPack() {
// This test case aims to demonstrate this scenario:
// - a change request is pending at one node.
// - that request can be packed by PackOwnRequests and sent to other nodes.
// - when some other node allow to propose a block, it will pack all those
// 'own' requests from others into the block's payload.
// - when all nodes receive that block, all pending requests (including
// those 'own' requests) would be cleaned.
var (
req = s.Require()
lambda = 250 * time.Millisecond
)
_, genesisNodes, err := NewKeys(20)
req.NoError(err)
st := NewState(1, genesisNodes, lambda, &common.NullLogger{}, false)
st1 := NewState(1, genesisNodes, lambda, &common.NullLogger{}, false)
req.NoError(st.Equal(st1))
// Make configuration changes.
s.makeConfigChanges(st)
// Add new CRS.
crs := common.NewRandomHash()
req.NoError(st.ProposeCRS(2, crs))
// Add new node.
prvKey, err := ecdsa.NewPrivateKey()
req.NoError(err)
pubKey := prvKey.PublicKey()
st.RequestChange(StateAddNode, pubKey)
// Add DKG stuffs.
masterPubKey := s.newDKGMasterPublicKey(2, 0)
ready := s.newDKGMPKReady(2, 0)
comp := s.newDKGComplaint(2, 0)
final := s.newDKGFinal(2, 0)
s.makeDKGChanges(st, masterPubKey, ready, comp, final)
// Pack those changes into a byte stream, and pass it to other State
// instance.
packed, err := st.PackOwnRequests()
req.NoError(err)
req.NotEmpty(packed)
// The second attempt to pack would get empty result.
emptyPackedAsByte, err := st.PackOwnRequests()
req.NoError(err)
emptyPacked, err := st.unpackRequests(emptyPackedAsByte)
req.NoError(err)
req.Empty(emptyPacked)
// Pass it to others.
req.NoError(st1.AddRequestsFromOthers(packed))
// These two instance are equal now, because their pending change requests
// are synced.
req.NoError(st.Equal(st1))
// Make them apply those pending changes.
applyChangesForRemoteState := func(s *State) {
p, err := s.PackRequests()
req.NoError(err)
req.NotEmpty(p)
req.NoError(s.Apply(p))
}
applyChangesForRemoteState(st)
applyChangesForRemoteState(st1)
// They should be equal after applying those changes.
req.NoError(st.Equal(st1))
}
func (s *StateTestSuite) TestUnmatchedResetCount() {
_, genesisNodes, err := NewKeys(20)
s.Require().NoError(err)
st := NewState(1, genesisNodes, 100*time.Millisecond,
&common.NullLogger{}, true)
// Make sure the case in older version without reset won't fail.
mpk := s.newDKGMasterPublicKey(1, 0)
ready := s.newDKGMPKReady(1, 0)
comp := s.newDKGComplaint(1, 0)
final := s.newDKGFinal(1, 0)
s.Require().NoError(st.RequestChange(StateAddDKGMasterPublicKey, mpk))
s.Require().NoError(st.RequestChange(StateAddDKGMPKReady, ready))
s.Require().NoError(st.RequestChange(StateAddDKGComplaint, comp))
s.Require().NoError(st.RequestChange(StateAddDKGFinal, final))
// Make round 1 reset twice.
s.Require().NoError(st.RequestChange(StateResetDKG, common.NewRandomHash()))
s.Require().NoError(st.RequestChange(StateResetDKG, common.NewRandomHash()))
s.Require().Equal(st.dkgResetCount[1], uint64(2))
s.Require().EqualError(ErrChangeWontApply, st.RequestChange(
StateAddDKGMasterPublicKey, mpk).Error())
s.Require().EqualError(ErrChangeWontApply, st.RequestChange(
StateAddDKGMPKReady, ready).Error())
s.Require().EqualError(ErrChangeWontApply, st.RequestChange(
StateAddDKGComplaint, comp).Error())
s.Require().EqualError(ErrChangeWontApply, st.RequestChange(
StateAddDKGFinal, final).Error())
mpk = s.newDKGMasterPublicKey(1, 2)
ready = s.newDKGMPKReady(1, 2)
comp = s.newDKGComplaint(1, 2)
final = s.newDKGFinal(1, 2)
s.Require().NoError(st.RequestChange(StateAddDKGMasterPublicKey, mpk))
s.Require().NoError(st.RequestChange(StateAddDKGMPKReady, ready))
s.Require().NoError(st.RequestChange(StateAddDKGComplaint, comp))
s.Require().NoError(st.RequestChange(StateAddDKGFinal, final))
}
func TestState(t *testing.T) {
suite.Run(t, new(StateTestSuite))
}