- package consensus
-
- import (
- "context"
- "fmt"
- "os"
- "path"
- "sync"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- dbm "github.com/tendermint/tm-db"
-
- abciclient "github.com/tendermint/tendermint/abci/client"
- "github.com/tendermint/tendermint/abci/example/kvstore"
- abci "github.com/tendermint/tendermint/abci/types"
- "github.com/tendermint/tendermint/internal/eventbus"
- "github.com/tendermint/tendermint/internal/evidence"
- "github.com/tendermint/tendermint/internal/mempool"
- "github.com/tendermint/tendermint/internal/p2p"
- sm "github.com/tendermint/tendermint/internal/state"
- "github.com/tendermint/tendermint/internal/store"
- "github.com/tendermint/tendermint/internal/test/factory"
- "github.com/tendermint/tendermint/libs/log"
- tmtime "github.com/tendermint/tendermint/libs/time"
- tmcons "github.com/tendermint/tendermint/proto/tendermint/consensus"
- tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
- "github.com/tendermint/tendermint/types"
- )
-
- // Byzantine node sends two different prevotes (nil and blockID) to the same
- // validator.
- func TestByzantinePrevoteEquivocation(t *testing.T) {
- // empirically, this test either passes in <1s or hits some
- // kind of deadlock and hit the larger timeout. This timeout
- // can be extended a bunch if needed, but it's good to avoid
- // falling back to a much coarser timeout
- ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
- defer cancel()
-
- config := configSetup(t)
-
- nValidators := 4
- prevoteHeight := int64(2)
- testName := "consensus_byzantine_test"
- tickerFunc := newMockTickerFunc(true)
-
- valSet, privVals := factory.ValidatorSet(ctx, t, nValidators, 30)
- genDoc := factory.GenesisDoc(config, time.Now(), valSet.Validators, nil)
- states := make([]*State, nValidators)
-
- for i := 0; i < nValidators; i++ {
- func() {
- logger := consensusLogger().With("test", "byzantine", "validator", i)
- stateDB := dbm.NewMemDB() // each state needs its own db
- stateStore := sm.NewStore(stateDB)
- state, err := sm.MakeGenesisState(genDoc)
- require.NoError(t, err)
- require.NoError(t, stateStore.Save(state))
-
- thisConfig, err := ResetConfig(t.TempDir(), fmt.Sprintf("%s_%d", testName, i))
- require.NoError(t, err)
-
- defer os.RemoveAll(thisConfig.RootDir)
-
- ensureDir(t, path.Dir(thisConfig.Consensus.WalFile()), 0700) // dir for wal
- app := kvstore.NewApplication()
- vals := types.TM2PB.ValidatorUpdates(state.Validators)
- app.InitChain(abci.RequestInitChain{Validators: vals})
-
- blockDB := dbm.NewMemDB()
- blockStore := store.NewBlockStore(blockDB)
-
- // one for mempool, one for consensus
- proxyAppConnMem := abciclient.NewLocalClient(logger, app)
- proxyAppConnCon := abciclient.NewLocalClient(logger, app)
-
- // Make Mempool
- mempool := mempool.NewTxMempool(
- log.NewNopLogger().With("module", "mempool"),
- thisConfig.Mempool,
- proxyAppConnMem,
- )
- if thisConfig.Consensus.WaitForTxs() {
- mempool.EnableTxsAvailable()
- }
-
- eventBus := eventbus.NewDefault(log.NewNopLogger().With("module", "events"))
- require.NoError(t, eventBus.Start(ctx))
-
- // Make a full instance of the evidence pool
- evidenceDB := dbm.NewMemDB()
- evpool := evidence.NewPool(logger.With("module", "evidence"), evidenceDB, stateStore, blockStore, evidence.NopMetrics(), eventBus)
-
- // Make State
- blockExec := sm.NewBlockExecutor(stateStore, log.NewNopLogger(), proxyAppConnCon, mempool, evpool, blockStore, eventBus)
- cs, err := NewState(ctx, logger, thisConfig.Consensus, stateStore, blockExec, blockStore, mempool, evpool, eventBus)
- require.NoError(t, err)
- // set private validator
- pv := privVals[i]
- cs.SetPrivValidator(ctx, pv)
-
- cs.SetTimeoutTicker(tickerFunc())
-
- states[i] = cs
- }()
- }
-
- rts := setup(ctx, t, nValidators, states, 512) // buffer must be large enough to not deadlock
-
- var bzNodeID types.NodeID
-
- // Set the first state's reactor as the dedicated byzantine reactor and grab
- // the NodeID that corresponds to the state so we can reference the reactor.
- bzNodeState := states[0]
- for nID, s := range rts.states {
- if s == bzNodeState {
- bzNodeID = nID
- break
- }
- }
-
- bzReactor := rts.reactors[bzNodeID]
-
- // alter prevote so that the byzantine node double votes when height is 2
- bzNodeState.doPrevote = func(ctx context.Context, height int64, round int32) {
- // allow first height to happen normally so that byzantine validator is no longer proposer
- if height == prevoteHeight {
- prevote1, err := bzNodeState.signVote(ctx,
- tmproto.PrevoteType,
- bzNodeState.ProposalBlock.Hash(),
- bzNodeState.ProposalBlockParts.Header(),
- )
- require.NoError(t, err)
-
- prevote2, err := bzNodeState.signVote(ctx, tmproto.PrevoteType, nil, types.PartSetHeader{})
- require.NoError(t, err)
-
- // send two votes to all peers (1st to one half, 2nd to another half)
- i := 0
- for _, ps := range bzReactor.peers {
- if i < len(bzReactor.peers)/2 {
- require.NoError(t, bzReactor.voteCh.Send(ctx,
- p2p.Envelope{
- To: ps.peerID,
- Message: &tmcons.Vote{
- Vote: prevote1.ToProto(),
- },
- }))
- } else {
- require.NoError(t, bzReactor.voteCh.Send(ctx,
- p2p.Envelope{
- To: ps.peerID,
- Message: &tmcons.Vote{
- Vote: prevote2.ToProto(),
- },
- }))
- }
-
- i++
- }
- } else {
- bzNodeState.defaultDoPrevote(ctx, height, round)
- }
- }
-
- // Introducing a lazy proposer means that the time of the block committed is
- // different to the timestamp that the other nodes have. This tests to ensure
- // that the evidence that finally gets proposed will have a valid timestamp.
- // lazyProposer := states[1]
- lazyNodeState := states[1]
-
- lazyNodeState.decideProposal = func(ctx context.Context, height int64, round int32) {
- require.NotNil(t, lazyNodeState.privValidator)
-
- var commit *types.Commit
- switch {
- case lazyNodeState.Height == lazyNodeState.state.InitialHeight:
- // We're creating a proposal for the first block.
- // The commit is empty, but not nil.
- commit = types.NewCommit(0, 0, types.BlockID{}, nil)
- case lazyNodeState.LastCommit.HasTwoThirdsMajority():
- // Make the commit from LastCommit
- commit = lazyNodeState.LastCommit.MakeCommit()
- default: // This shouldn't happen.
- lazyNodeState.logger.Error("enterPropose: Cannot propose anything: No commit for the previous block")
- return
- }
-
- // omit the last signature in the commit
- commit.Signatures[len(commit.Signatures)-1] = types.NewCommitSigAbsent()
-
- if lazyNodeState.privValidatorPubKey == nil {
- // If this node is a validator & proposer in the current round, it will
- // miss the opportunity to create a block.
- lazyNodeState.logger.Error("enterPropose", "err", errPubKeyIsNotSet)
- return
- }
- proposerAddr := lazyNodeState.privValidatorPubKey.Address()
-
- block, err := lazyNodeState.blockExec.CreateProposalBlock(
- ctx, lazyNodeState.Height, lazyNodeState.state, commit, proposerAddr, nil)
- require.NoError(t, err)
- blockParts, err := block.MakePartSet(types.BlockPartSizeBytes)
- require.NoError(t, err)
-
- // Flush the WAL. Otherwise, we may not recompute the same proposal to sign,
- // and the privValidator will refuse to sign anything.
- if err := lazyNodeState.wal.FlushAndSync(); err != nil {
- lazyNodeState.logger.Error("error flushing to disk")
- }
-
- // Make proposal
- propBlockID := types.BlockID{Hash: block.Hash(), PartSetHeader: blockParts.Header()}
- proposal := types.NewProposal(height, round, lazyNodeState.ValidRound, propBlockID, block.Header.Time)
- p := proposal.ToProto()
- if err := lazyNodeState.privValidator.SignProposal(ctx, lazyNodeState.state.ChainID, p); err == nil {
- proposal.Signature = p.Signature
-
- // send proposal and block parts on internal msg queue
- lazyNodeState.sendInternalMessage(ctx, msgInfo{&ProposalMessage{proposal}, "", tmtime.Now()})
- for i := 0; i < int(blockParts.Total()); i++ {
- part := blockParts.GetPart(i)
- lazyNodeState.sendInternalMessage(ctx, msgInfo{&BlockPartMessage{
- lazyNodeState.Height, lazyNodeState.Round, part,
- }, "", tmtime.Now()})
- }
- } else if !lazyNodeState.replayMode {
- lazyNodeState.logger.Error("enterPropose: Error signing proposal", "height", height, "round", round, "err", err)
- }
- }
-
- for _, reactor := range rts.reactors {
- reactor.SwitchToConsensus(ctx, reactor.state.GetState(), false)
- }
-
- // Evidence should be submitted and committed at the third height but
- // we will check the first six just in case
- evidenceFromEachValidator := make([]types.Evidence, nValidators)
-
- var wg sync.WaitGroup
- i := 0
- subctx, subcancel := context.WithCancel(ctx)
- defer subcancel()
- for _, sub := range rts.subs {
- wg.Add(1)
-
- go func(j int, s eventbus.Subscription) {
- defer wg.Done()
- for {
- if subctx.Err() != nil {
- return
- }
-
- msg, err := s.Next(subctx)
- if subctx.Err() != nil {
- return
- }
-
- if err != nil {
- t.Errorf("waiting for subscription: %v", err)
- subcancel()
- return
- }
-
- require.NotNil(t, msg)
- block := msg.Data().(types.EventDataNewBlock).Block
- if len(block.Evidence) != 0 {
- evidenceFromEachValidator[j] = block.Evidence[0]
- return
- }
- }
- }(i, sub)
- i++
- }
-
- wg.Wait()
-
- // don't run more assertions if we've encountered a timeout
- select {
- case <-subctx.Done():
- t.Fatal("encountered timeout")
- default:
- }
-
- pubkey, err := bzNodeState.privValidator.GetPubKey(ctx)
- require.NoError(t, err)
-
- for idx, ev := range evidenceFromEachValidator {
- require.NotNil(t, ev, idx)
- ev, ok := ev.(*types.DuplicateVoteEvidence)
- require.True(t, ok)
- assert.Equal(t, pubkey.Address(), ev.VoteA.ValidatorAddress)
- assert.Equal(t, prevoteHeight, ev.Height())
- }
- }
|