package v0
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
abci "github.com/tendermint/tendermint/abci/types"
|
|
cfg "github.com/tendermint/tendermint/config"
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
"github.com/tendermint/tendermint/mempool/mock"
|
|
"github.com/tendermint/tendermint/p2p"
|
|
bcproto "github.com/tendermint/tendermint/proto/tendermint/blockchain"
|
|
"github.com/tendermint/tendermint/proxy"
|
|
sm "github.com/tendermint/tendermint/state"
|
|
"github.com/tendermint/tendermint/store"
|
|
"github.com/tendermint/tendermint/types"
|
|
dbm "github.com/tendermint/tm-db"
|
|
)
|
|
|
|
var rng = rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
|
|
type reactorTestSuite struct {
|
|
reactor *Reactor
|
|
app proxy.AppConns
|
|
|
|
peerID p2p.NodeID
|
|
|
|
blockchainChannel *p2p.Channel
|
|
blockchainInCh chan p2p.Envelope
|
|
blockchainOutCh chan p2p.Envelope
|
|
blockchainPeerErrCh chan p2p.PeerError
|
|
|
|
peerUpdatesCh chan p2p.PeerUpdate
|
|
peerUpdates *p2p.PeerUpdatesCh
|
|
}
|
|
|
|
func setup(
|
|
t *testing.T,
|
|
genDoc *types.GenesisDoc,
|
|
privVals []types.PrivValidator,
|
|
maxBlockHeight int64,
|
|
chBuf uint,
|
|
) *reactorTestSuite {
|
|
t.Helper()
|
|
|
|
require.Len(t, privVals, 1, "only one validator can be supported")
|
|
|
|
app := &abci.BaseApplication{}
|
|
cc := proxy.NewLocalClientCreator(app)
|
|
|
|
proxyApp := proxy.NewAppConns(cc)
|
|
require.NoError(t, proxyApp.Start())
|
|
|
|
blockDB := dbm.NewMemDB()
|
|
stateDB := dbm.NewMemDB()
|
|
stateStore := sm.NewStore(stateDB)
|
|
blockStore := store.NewBlockStore(blockDB)
|
|
|
|
state, err := stateStore.LoadFromDBOrGenesisDoc(genDoc)
|
|
require.NoError(t, err)
|
|
|
|
fastSync := true
|
|
db := dbm.NewMemDB()
|
|
stateStore = sm.NewStore(db)
|
|
|
|
blockExec := sm.NewBlockExecutor(
|
|
stateStore,
|
|
log.TestingLogger(),
|
|
proxyApp.Consensus(),
|
|
mock.Mempool{},
|
|
sm.EmptyEvidencePool{},
|
|
)
|
|
require.NoError(t, stateStore.Save(state))
|
|
|
|
for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ {
|
|
lastCommit := types.NewCommit(blockHeight-1, 0, types.BlockID{}, nil)
|
|
|
|
if blockHeight > 1 {
|
|
lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1)
|
|
lastBlock := blockStore.LoadBlock(blockHeight - 1)
|
|
|
|
vote, err := types.MakeVote(
|
|
lastBlock.Header.Height,
|
|
lastBlockMeta.BlockID,
|
|
state.Validators,
|
|
privVals[0],
|
|
lastBlock.Header.ChainID,
|
|
time.Now(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
lastCommit = types.NewCommit(
|
|
vote.Height,
|
|
vote.Round,
|
|
lastBlockMeta.BlockID,
|
|
[]types.CommitSig{vote.CommitSig()},
|
|
)
|
|
}
|
|
|
|
thisBlock := makeBlock(blockHeight, state, lastCommit)
|
|
thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes)
|
|
blockID := types.BlockID{Hash: thisBlock.Hash(), PartSetHeader: thisParts.Header()}
|
|
|
|
state, _, err = blockExec.ApplyBlock(state, blockID, thisBlock)
|
|
require.NoError(t, err)
|
|
|
|
blockStore.SaveBlock(thisBlock, thisParts, lastCommit)
|
|
}
|
|
|
|
pID := make([]byte, 16)
|
|
_, err = rng.Read(pID)
|
|
require.NoError(t, err)
|
|
|
|
peerUpdatesCh := make(chan p2p.PeerUpdate, chBuf)
|
|
|
|
rts := &reactorTestSuite{
|
|
app: proxyApp,
|
|
blockchainInCh: make(chan p2p.Envelope, chBuf),
|
|
blockchainOutCh: make(chan p2p.Envelope, chBuf),
|
|
blockchainPeerErrCh: make(chan p2p.PeerError, chBuf),
|
|
peerUpdatesCh: peerUpdatesCh,
|
|
peerUpdates: p2p.NewPeerUpdates(peerUpdatesCh),
|
|
peerID: p2p.NodeID(fmt.Sprintf("%x", pID)),
|
|
}
|
|
|
|
rts.blockchainChannel = p2p.NewChannel(
|
|
BlockchainChannel,
|
|
new(bcproto.Message),
|
|
rts.blockchainInCh,
|
|
rts.blockchainOutCh,
|
|
rts.blockchainPeerErrCh,
|
|
)
|
|
|
|
reactor, err := NewReactor(
|
|
log.TestingLogger().With("module", "blockchain", "node", rts.peerID),
|
|
state.Copy(),
|
|
blockExec,
|
|
blockStore,
|
|
nil,
|
|
rts.blockchainChannel,
|
|
rts.peerUpdates,
|
|
fastSync,
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
rts.reactor = reactor
|
|
|
|
require.NoError(t, rts.reactor.Start())
|
|
require.True(t, rts.reactor.IsRunning())
|
|
|
|
t.Cleanup(func() {
|
|
require.NoError(t, rts.reactor.Stop())
|
|
require.NoError(t, rts.app.Stop())
|
|
require.False(t, rts.reactor.IsRunning())
|
|
})
|
|
|
|
return rts
|
|
}
|
|
|
|
func simulateRouter(primary *reactorTestSuite, suites []*reactorTestSuite, dropChErr bool) {
|
|
// create a mapping for efficient suite lookup by peer ID
|
|
suitesByPeerID := make(map[p2p.NodeID]*reactorTestSuite)
|
|
for _, suite := range suites {
|
|
suitesByPeerID[suite.peerID] = suite
|
|
}
|
|
|
|
// Simulate a router by listening for all outbound envelopes and proxying the
|
|
// envelope to the respective peer (suite).
|
|
go func() {
|
|
for envelope := range primary.blockchainOutCh {
|
|
if envelope.Broadcast {
|
|
for _, s := range suites {
|
|
// broadcast to everyone except source
|
|
if s.peerID != primary.peerID {
|
|
s.blockchainInCh <- p2p.Envelope{
|
|
From: primary.peerID,
|
|
To: s.peerID,
|
|
Message: envelope.Message,
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
suitesByPeerID[envelope.To].blockchainInCh <- p2p.Envelope{
|
|
From: primary.peerID,
|
|
To: envelope.To,
|
|
Message: envelope.Message,
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
for pErr := range primary.blockchainPeerErrCh {
|
|
if dropChErr {
|
|
primary.reactor.Logger.Debug("dropped peer error", "err", pErr.Err)
|
|
} else {
|
|
primary.peerUpdatesCh <- p2p.PeerUpdate{
|
|
PeerID: pErr.PeerID,
|
|
Status: p2p.PeerStatusRemoved,
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func TestReactor_AbruptDisconnect(t *testing.T) {
|
|
config := cfg.ResetTestRoot("blockchain_reactor_test")
|
|
defer os.RemoveAll(config.RootDir)
|
|
|
|
genDoc, privVals := randGenesisDoc(config, 1, false, 30)
|
|
maxBlockHeight := int64(64)
|
|
testSuites := []*reactorTestSuite{
|
|
setup(t, genDoc, privVals, maxBlockHeight, 0),
|
|
setup(t, genDoc, privVals, 0, 0),
|
|
}
|
|
|
|
require.Equal(t, maxBlockHeight, testSuites[0].reactor.store.Height())
|
|
|
|
for _, s := range testSuites {
|
|
simulateRouter(s, testSuites, true)
|
|
|
|
// connect reactor to every other reactor
|
|
for _, ss := range testSuites {
|
|
if s.peerID != ss.peerID {
|
|
s.peerUpdatesCh <- p2p.PeerUpdate{
|
|
Status: p2p.PeerStatusUp,
|
|
PeerID: ss.peerID,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
secondaryPool := testSuites[1].reactor.pool
|
|
require.Eventually(
|
|
t,
|
|
func() bool {
|
|
height, _, _ := secondaryPool.GetStatus()
|
|
return secondaryPool.MaxPeerHeight() > 0 && height > 0 && height < 10
|
|
},
|
|
10*time.Second,
|
|
10*time.Millisecond,
|
|
"expected node to be partially synced",
|
|
)
|
|
|
|
// Remove synced node from the syncing node which should not result in any
|
|
// deadlocks or race conditions within the context of poolRoutine.
|
|
testSuites[1].peerUpdatesCh <- p2p.PeerUpdate{
|
|
Status: p2p.PeerStatusDown,
|
|
PeerID: testSuites[0].peerID,
|
|
}
|
|
}
|
|
|
|
func TestReactor_NoBlockResponse(t *testing.T) {
|
|
config := cfg.ResetTestRoot("blockchain_reactor_test")
|
|
defer os.RemoveAll(config.RootDir)
|
|
|
|
genDoc, privVals := randGenesisDoc(config, 1, false, 30)
|
|
maxBlockHeight := int64(65)
|
|
testSuites := []*reactorTestSuite{
|
|
setup(t, genDoc, privVals, maxBlockHeight, 0),
|
|
setup(t, genDoc, privVals, 0, 0),
|
|
}
|
|
|
|
require.Equal(t, maxBlockHeight, testSuites[0].reactor.store.Height())
|
|
|
|
for _, s := range testSuites {
|
|
simulateRouter(s, testSuites, true)
|
|
|
|
// connect reactor to every other reactor
|
|
for _, ss := range testSuites {
|
|
if s.peerID != ss.peerID {
|
|
s.peerUpdatesCh <- p2p.PeerUpdate{
|
|
Status: p2p.PeerStatusUp,
|
|
PeerID: ss.peerID,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
testCases := []struct {
|
|
height int64
|
|
existent bool
|
|
}{
|
|
{maxBlockHeight + 2, false},
|
|
{10, true},
|
|
{1, true},
|
|
{100, false},
|
|
}
|
|
|
|
secondaryPool := testSuites[1].reactor.pool
|
|
require.Eventually(
|
|
t,
|
|
func() bool { return secondaryPool.MaxPeerHeight() > 0 && secondaryPool.IsCaughtUp() },
|
|
10*time.Second,
|
|
10*time.Millisecond,
|
|
"expected node to be fully synced",
|
|
)
|
|
|
|
for _, tc := range testCases {
|
|
block := testSuites[1].reactor.store.LoadBlock(tc.height)
|
|
if tc.existent {
|
|
require.True(t, block != nil)
|
|
} else {
|
|
require.Nil(t, block)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReactor_BadBlockStopsPeer(t *testing.T) {
|
|
config := cfg.ResetTestRoot("blockchain_reactor_test")
|
|
defer os.RemoveAll(config.RootDir)
|
|
|
|
maxBlockHeight := int64(48)
|
|
genDoc, privVals := randGenesisDoc(config, 1, false, 30)
|
|
|
|
testSuites := []*reactorTestSuite{
|
|
setup(t, genDoc, privVals, maxBlockHeight, 1000), // fully synced node
|
|
setup(t, genDoc, privVals, 0, 1000),
|
|
setup(t, genDoc, privVals, 0, 1000),
|
|
setup(t, genDoc, privVals, 0, 1000),
|
|
setup(t, genDoc, privVals, 0, 1000), // new node
|
|
}
|
|
|
|
require.Equal(t, maxBlockHeight, testSuites[0].reactor.store.Height())
|
|
|
|
for _, s := range testSuites[:len(testSuites)-1] {
|
|
simulateRouter(s, testSuites, true)
|
|
|
|
// connect reactor to every other reactor except the new node
|
|
for _, ss := range testSuites[:len(testSuites)-1] {
|
|
if s.peerID != ss.peerID {
|
|
s.peerUpdatesCh <- p2p.PeerUpdate{
|
|
Status: p2p.PeerStatusUp,
|
|
PeerID: ss.peerID,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
require.Eventually(
|
|
t,
|
|
func() bool {
|
|
caughtUp := true
|
|
for _, s := range testSuites[1 : len(testSuites)-1] {
|
|
if s.reactor.pool.MaxPeerHeight() == 0 || !s.reactor.pool.IsCaughtUp() {
|
|
caughtUp = false
|
|
}
|
|
}
|
|
|
|
return caughtUp
|
|
},
|
|
10*time.Minute,
|
|
10*time.Millisecond,
|
|
"expected all nodes to be fully synced",
|
|
)
|
|
|
|
for _, s := range testSuites[:len(testSuites)-1] {
|
|
require.Len(t, s.reactor.pool.peers, 3)
|
|
}
|
|
|
|
// Mark testSuites[3] as an invalid peer which will cause newSuite to disconnect
|
|
// from this peer.
|
|
otherGenDoc, otherPrivVals := randGenesisDoc(config, 1, false, 30)
|
|
otherSuite := setup(t, otherGenDoc, otherPrivVals, maxBlockHeight, 0)
|
|
testSuites[3].reactor.store = otherSuite.reactor.store
|
|
|
|
// add a fake peer just so we do not wait for the consensus ticker to timeout
|
|
otherSuite.reactor.pool.SetPeerRange("00ff", 10, 10)
|
|
|
|
// start the new peer's faux router
|
|
newSuite := testSuites[len(testSuites)-1]
|
|
simulateRouter(newSuite, testSuites, false)
|
|
|
|
// connect all nodes to the new peer
|
|
for _, s := range testSuites[:len(testSuites)-1] {
|
|
newSuite.peerUpdatesCh <- p2p.PeerUpdate{
|
|
Status: p2p.PeerStatusUp,
|
|
PeerID: s.peerID,
|
|
}
|
|
}
|
|
|
|
// wait for the new peer to catch up and become fully synced
|
|
require.Eventually(
|
|
t,
|
|
func() bool { return newSuite.reactor.pool.MaxPeerHeight() > 0 && newSuite.reactor.pool.IsCaughtUp() },
|
|
10*time.Minute,
|
|
10*time.Millisecond,
|
|
"expected new node to be fully synced",
|
|
)
|
|
|
|
require.Eventuallyf(
|
|
t,
|
|
func() bool { return len(newSuite.reactor.pool.peers) < len(testSuites)-1 },
|
|
10*time.Minute,
|
|
10*time.Millisecond,
|
|
"invalid number of peers; expected < %d, got: %d",
|
|
len(testSuites)-1,
|
|
len(newSuite.reactor.pool.peers),
|
|
)
|
|
}
|