CVE-2021-21271: Uncontrolled Resource Consumption (CWE-400)
value: Tendermint Core is an open source Byzantine Fault Tolerant (BFT) middleware that takes a state transition machine - written in any programming language - and securely replicates it on many machines. Tendermint Core v0.34.0 introduced a new way of handling evidence of misbehavior. As part of this, we added a new Timestamp field to Evidence structs. This timestamp would be calculated using the same algorithm that is used when a block is created and proposed. (This algorithm relies on the timestamp of the last commit from this specific block.) In Tendermint Core v0.34.0-v0.34.2, the consensus reactor is responsible for forming DuplicateVoteEvidence whenever double signs are observed. However, the current block is still “in flight” when it is being formed by the consensus reactor. It hasn’t been finalized through network consensus yet. This means that different nodes in the network may observe different “last commits” when assigning a timestamp to DuplicateVoteEvidence. In turn, different nodes could form DuplicateVoteEvidence objects at the same height but with different timestamps. One DuplicateVoteEvidence object (with one timestamp) will then eventually get finalized in the block, but this means that any DuplicateVoteEvidence with a different timestamp is considered invalid. Any node that formed invalid DuplicateVoteEvidence will continue to propose invalid evidence; its peers may see this, and choose to disconnect from this node. This bug means that double signs are DoS vectors in Tendermint Core v0.34.0-v0.34.2. Tendermint Core v0.34.3 is a security release which fixes this bug. As of v0.34.3, DuplicateVoteEvidence is no longer formed by the consensus reactor; rather, the consensus reactor passes the Votes themselves into the EvidencePool, which is now responsible for forming DuplicateVoteEvidence. The EvidencePool has timestamp info that should be consistent across the network, which means that DuplicateVoteEvidence formed in this reactor should have consistent timestamps. This release changes the API between the consensus and evidence reactors.
CWE description: The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
CVSS v3 base score: 6.5
Published: 2021-01-26T21:15Z
Repository: https://github.com/tendermint/tendermint
Language: Go
Files changed: consensus/byzantine_test.go, consensus/state.go, evidence/pool.go, evidence/pool_test.go, node/node_test.go, state/services.go, test/maverick/consensus/state.go
Commit date: 2021-01-19 16:00:02 +0100
Commit message: use correct source of evidence time

Conflicting votes are now sent to the evidence pool to form duplicate vote evidence only once
the height of the evidence is finished and the time of the block finalised.

Diff: diff --git a/consensus/byzantine_test.go b/consensus/byzantine_test.go
index a82242db2..f5e372ae2 100644
--- a/consensus/byzantine_test.go
+++ b/consensus/byzantine_test.go
@@ -152,10 +152,76 @@ func TestByzantinePrevoteEquivocation(t *testing.T) {
 			bcs.Logger.Info("Behaving normally")
 			bcs.defaultDoPrevote(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 := css[1]
+
+	lazyProposer.decideProposal = func(height int64, round int32) {
+		lazyProposer.Logger.Info("Lazy Proposer proposing condensed commit")
+		if lazyProposer.privValidator == nil {
+			panic("entered createProposalBlock with privValidator being nil")
+		}
+
+		var commit *types.Commit
+		switch {
+		case lazyProposer.Height == lazyProposer.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 lazyProposer.LastCommit.HasTwoThirdsMajority():
+			// Make the commit from LastCommit
+			commit = lazyProposer.LastCommit.MakeCommit()
+		default: // This shouldn't happen.
+			lazyProposer.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 lazyProposer.privValidatorPubKey == nil {
+			// If this node is a validator & proposer in the current round, it will
+			// miss the opportunity to create a block.
+			lazyProposer.Logger.Error(fmt.Sprintf("enterPropose: %v", errPubKeyIsNotSet))
+			return
+		}
+		proposerAddr := lazyProposer.privValidatorPubKey.Address()
+
+		block, blockParts := lazyProposer.blockExec.CreateProposalBlock(
+			lazyProposer.Height, lazyProposer.state, commit, proposerAddr,
+		)
+
+		// Flush the WAL. Otherwise, we may not recompute the same proposal to sign,
+		// and the privValidator will refuse to sign anything.
+		if err := lazyProposer.wal.FlushAndSync(); err != nil {
+			lazyProposer.Logger.Error("Error flushing to disk")
+		}
+
+		// Make proposal
+		propBlockID := types.BlockID{Hash: block.Hash(), PartSetHeader: blockParts.Header()}
+		proposal := types.NewProposal(height, round, lazyProposer.ValidRound, propBlockID)
+		p := proposal.ToProto()
+		if err := lazyProposer.privValidator.SignProposal(lazyProposer.state.ChainID, p); err == nil {
+			proposal.Signature = p.Signature
+
+			// send proposal and block parts on internal msg queue
+			lazyProposer.sendInternalMessage(msgInfo{&ProposalMessage{proposal}, ""})
+			for i := 0; i < int(blockParts.Total()); i++ {
+				part := blockParts.GetPart(i)
+				lazyProposer.sendInternalMessage(msgInfo{&BlockPartMessage{lazyProposer.Height, lazyProposer.Round, part}, ""})
+			}
+			lazyProposer.Logger.Info("Signed proposal", "height", height, "round", round, "proposal", proposal)
+			lazyProposer.Logger.Debug(fmt.Sprintf("Signed proposal block: %v", block))
+		} else if !lazyProposer.replayMode {
+			lazyProposer.Logger.Error("enterPropose: Error signing proposal", "height", height, "round", round, "err", err)
+		}
+	}
+
 	// start the consensus reactors
 	for i := 0; i < nValidators; i++ {
 		s := reactors[i].conS.GetState()
 		reactors[i].SwitchToConsensus(s, false)
 	}
diff --git a/consensus/state.go b/consensus/state.go
index a046840e2..f263293df 100644
--- a/consensus/state.go
+++ b/consensus/state.go
@@ -71,13 +71,12 @@ type txNotifier interface {
 	TxsAvailable() <-chan struct{}
 }
 
 // interface to the evidence pool
 type evidencePool interface {
-	// Adds consensus based evidence to the evidence pool. This function differs to
-	// AddEvidence by bypassing verification and adding it immediately to the pool
-	AddEvidenceFromConsensus(types.Evidence) error
+	// reports conflicting votes to the evidence pool to be processed into evidence
+	ReportConflictingVotes(voteA, voteB *types.Vote)
 }
 
 // State handles execution of the consensus algorithm.
 // It processes votes and proposals, and upon reaching agreement,
 // commits blocks to the chain and executes them against the application.
@@ -1863,25 +1862,16 @@ func (cs *State) tryAddVote(vote *types.Vote, peerID p2p.ID) (bool, error) {
 					vote.Round,
 					"type",
 					vote.Type)
 				return added, err
 			}
-			var timestamp time.Time
-			if voteErr.VoteA.Height == cs.state.InitialHeight {
-				timestamp = cs.state.LastBlockTime // genesis time
-			} else {
-				timestamp = sm.MedianTime(cs.LastCommit.MakeCommit(), cs.LastValidators)
-			}
-			// form duplicate vote evidence from the conflicting votes and send it across to the
-			// evidence pool
-			ev := types.NewDuplicateVoteEvidence(voteErr.VoteA, voteErr.VoteB, timestamp, cs.Validators)
-			evidenceErr := cs.evpool.AddEvidenceFromConsensus(ev)
-			if evidenceErr != nil {
-				cs.Logger.Error("Failed to add evidence to the evidence pool", "err", evidenceErr)
-			} else {
-				cs.Logger.Debug("Added evidence to the evidence pool", "ev", ev)
-			}
+			// report conflicting votes to the evidence pool
+			cs.evpool.ReportConflictingVotes(voteErr.VoteA, voteErr.VoteB)
+			cs.Logger.Info("Found and sent conflicting votes to the evidence pool",
+				"VoteA", voteErr.VoteA,
+				"VoteB", voteErr.VoteB,
+			)
 			return added, err
 		} else if err == types.ErrVoteNonDeterministicSignature {
 			cs.Logger.Debug("Vote has non-deterministic signature", "err", err)
 		} else {
 			// Either
diff --git a/evidence/pool.go b/evidence/pool.go
index f57490290..5b6b9ef7e 100644
--- a/evidence/pool.go
+++ b/evidence/pool.go
@@ -39,14 +39,14 @@ type Pool struct {
 	blockStore BlockStore
 
 	mtx sync.Mutex
 	// latest state
 	state sm.State
-	// evidence from consensus if buffered to this slice, awaiting until the next height
+	// evidence from consensus is buffered to this slice, awaiting until the next height
 	// before being flushed to the pool. This prevents broadcasting and proposing of
 	// evidence before the height with which the evidence happened is finished.
-	consensusBuffer []types.Evidence
+	consensusBuffer []duplicateVoteSet
 
 	pruningHeight int64
 	pruningTime   time.Time
 }
 
@@ -64,11 +64,11 @@ func NewPool(evidenceDB dbm.DB, stateDB sm.Store, blockStore BlockStore) (*Pool,
 		blockStore:      blockStore,
 		state:           state,
 		logger:          log.NewNopLogger(),
 		evidenceStore:   evidenceDB,
 		evidenceList:    clist.New(),
-		consensusBuffer: make([]types.Evidence, 0),
+		consensusBuffer: make([]duplicateVoteSet, 0),
 	}
 
 	// if pending evidence already in db, in event of prior failure, then check for expiration,
 	// update the size and load it back to the evidenceList
 	pool.pruningHeight, pool.pruningTime = pool.removeExpiredPendingEvidence()
@@ -94,35 +94,34 @@ func (evpool *Pool) PendingEvidence(maxBytes int64) ([]types.Evidence, int64) {
 		evpool.logger.Error("Unable to retrieve pending evidence", "err", err)
 	}
 	return evidence, size
 }
 
-// Update pulls the latest state to be used for expiration and evidence params and then prunes all expired evidence
+// Update takes both the new state and the evidence committed at that height and performs
+// the following operations:
+// 1. Take any conflicting votes from consensus and use the state's LastBlockTime to form
+//    DuplicateVoteEvidence and add it to the pool.
+// 2. Update the pool's state which contains evidence params relating to expiry.
+// 3. Moves pending evidence that has now been committed into the committed pool.
+// 4. Removes any expired evidence based on both height and time.
 func (evpool *Pool) Update(state sm.State, ev types.EvidenceList) {
 	// sanity check
 	if state.LastBlockHeight <= evpool.state.LastBlockHeight {
 		panic(fmt.Sprintf(
-			"Failed EvidencePool.Update new state height is less than or equal to previous state height: %d <= %d",
+			"failed EvidencePool.Update new state height is less than or equal to previous state height: %d <= %d",
 			state.LastBlockHeight,
 			evpool.state.LastBlockHeight,
 		))
 	}
-	evpool.logger.Info("Updating evidence pool", "last_block_height", state.LastBlockHeight,
+	evpool.logger.Debug("Updating evidence pool", "last_block_height", state.LastBlockHeight,
 		"last_block_time", state.LastBlockTime)
 
-	evpool.logger.Info(
-		"updating evidence pool",
-		"last_block_height", state.LastBlockHeight,
-		"last_block_time", state.LastBlockTime,
-	)
-
-	evpool.mtx.Lock()
-	// flush awaiting evidence from consensus into pool
-	evpool.flushConsensusBuffer()
+	// flush conflicting vote pairs from the buffer, producing DuplicateVoteEvidence and
+	// adding it to the pool
+	evpool.processConsensusBuffer(state)
 	// update state
-	evpool.state = state
-	evpool.mtx.Unlock()
+	evpool.updateState(state)
 
 	// move committed evidence out from the pending pool and into the committed pool
 	evpool.markEvidenceAsCommitted(ev)
 
 	// prune pending evidence when it has expired. This also updates when the next evidence will expire
@@ -136,11 +135,11 @@ func (evpool *Pool) Update(state sm.State, ev types.EvidenceList) {
 func (evpool *Pool) AddEvidence(ev types.Evidence) error {
 	evpool.logger.Debug("Attempting to add evidence", "ev", ev)
 
 	// We have already verified this piece of evidence - no need to do it again
 	if evpool.isPending(ev) {
-		evpool.logger.Info("Evidence already pending, ignoring this one", "ev", ev)
+		evpool.logger.Debug("Evidence already pending, ignoring this one", "ev", ev)
 		return nil
 	}
 
 	// check that the evidence isn't already committed
 	if evpool.isCommitted(ev) {
@@ -167,29 +166,26 @@ func (evpool *Pool) AddEvidence(ev types.Evidence) error {
 	evpool.logger.Info("Verified new evidence of byzantine behavior", "evidence", ev)
 
 	return nil
 }
 
-// AddEvidenceFromConsensus should be exposed only to the consensus reactor so it can add evidence
-// to the pool directly without the need for verification.
-func (evpool *Pool) AddEvidenceFromConsensus(ev types.Evidence) error {
-
-	// we already have this evidence, log this but don't return an error.
-	if evpool.isPending(ev) {
-		evpool.logger.Info("Evidence already pending, ignoring this one", "ev", ev)
-		return nil
-	}
-
-	// add evidence to a buffer which will pass the evidence to the pool at the following height.
-	// This avoids the issue of some nodes verifying and proposing evidence at a height where the
-	// block hasn't been committed on cause others to potentially fail.
+// ReportConflictingVotes takes two conflicting votes and forms duplicate vote evidence,
+// adding it eventually to the evidence pool.
+//
+// Duplicate vote attacks happen before the block is committed and the timestamp is
+// finalized, thus the evidence pool holds these votes in a buffer, forming the
+// evidence from them once consensus at that height has been reached and `Update()` with
+// the new state called.
+//
+// Votes are not verified.
+func (evpool *Pool) ReportConflictingVotes(voteA, voteB *types.Vote) {
 	evpool.mtx.Lock()
 	defer evpool.mtx.Unlock()
-	evpool.consensusBuffer = append(evpool.consensusBuffer, ev)
-	evpool.logger.Info("received new evidence of byzantine behavior from consensus", "evidence", ev)
-
-	return nil
+	evpool.consensusBuffer = append(evpool.consensusBuffer, duplicateVoteSet{
+		VoteA: voteA,
+		VoteB: voteB,
+	})
 }
 
 // CheckEvidence takes an array of evidence from a block and verifies all the evidence there.
 // If it has already verified the evidence then it jumps to the next one. It ensures that no
 // evidence has already been committed or is being proposed twice. It also adds any
@@ -206,11 +202,11 @@ func (evpool *Pool) CheckEvidence(evList types.EvidenceList) error {
 				return &types.ErrInvalidEvidence{Evidence: ev, Reason: errors.New("evidence was already committed")}
 			}
 
 			err := evpool.verify(ev)
 			if err != nil {
-				return &types.ErrInvalidEvidence{Evidence: ev, Reason: err}
+				return err
 			}
 
 			if err := evpool.addPendingEvidence(ev); err != nil {
 				// Something went wrong with adding the evidence but we already know it is valid
 				// hence we log an error and continue
@@ -378,11 +374,11 @@ func (evpool *Pool) removePendingEvidence(evidence types.Evidence) {
 	key := keyPending(evidence)
 	if err := evpool.evidenceStore.Delete(key); err != nil {
 		evpool.logger.Error("Unable to delete pending evidence", "err", err)
 	} else {
 		atomic.AddUint32(&evpool.evidenceSize, ^uint32(0))
-		evpool.logger.Info("Deleted pending evidence", "evidence", evidence)
+		evpool.logger.Debug("Deleted pending evidence", "evidence", evidence)
 	}
 }
 
 // markEvidenceAsCommitted processes all the evidence in the block, marking it as
 // committed and removing it from the pending database.
@@ -505,23 +501,95 @@ func (evpool *Pool) removeEvidenceFromList(
 			e.DetachPrev()
 		}
 	}
 }
 
-// flushConsensusBuffer moves the evidence produced from consensus into the evidence pool
-// and list so that it can be broadcasted and proposed
-func (evpool *Pool) flushConsensusBuffer() {
-	for _, ev := range evpool.consensusBuffer {
-		if err := evpool.addPendingEvidence(ev); err != nil {
+func (evpool *Pool) updateState(state sm.State) {
+	evpool.mtx.Lock()
+	defer evpool.mtx.Unlock()
+	evpool.state = state
+}
+
+// processConsensusBuffer converts all the duplicate votes witnessed from consensus
+// into DuplicateVoteEvidence. It sets the evidence timestamp to the block height
+// from the most recently committed block.
+// Evidence is then added to the pool so as to be ready to be broadcasted and proposed.
+func (evpool *Pool) processConsensusBuffer(state sm.State) {
+	evpool.mtx.Lock()
+	defer evpool.mtx.Unlock()
+	for _, voteSet := range evpool.consensusBuffer {
+
+		// Check the height of the conflicting votes and fetch the corresponding time and validator set
+		// to produce the valid evidence
+		var dve *types.DuplicateVoteEvidence
+		switch {
+		case voteSet.VoteA.Height == state.LastBlockHeight:
+			dve = types.NewDuplicateVoteEvidence(
+				voteSet.VoteA,
+				voteSet.VoteB,
+				state.LastBlockTime,
+				state.LastValidators,
+			)
+
+		case voteSet.VoteA.Height < state.LastBlockHeight:
+			valSet, err := evpool.stateDB.LoadValidators(voteSet.VoteA.Height)
+			if err != nil {
+				evpool.logger.Error("failed to load validator set for conflicting votes", "height",
+					voteSet.VoteA.Height, "err", err,
+				)
+				continue
+			}
+			blockMeta := evpool.blockStore.LoadBlockMeta(voteSet.VoteA.Height)
+			if blockMeta == nil {
+				evpool.logger.Error("failed to load block time for conflicting votes", "height", voteSet.VoteA.Height)
+				continue
+			}
+			dve = types.NewDuplicateVoteEvidence(
+				voteSet.VoteA,
+				voteSet.VoteB,
+				blockMeta.Header.Time,
+				valSet,
+			)
+
+		default:
+			// evidence pool shouldn't expect to get votes from consensus of a height that is above the current
+			// state. If this error is seen then perhaps consider keeping the votes in the buffer and retry
+			// in following heights
+			evpool.logger.Error("inbound duplicate votes from consensus are of a greater height than current state",
+				"duplicate vote height", voteSet.VoteA.Height,
+				"state.LastBlockHeight", state.LastBlockHeight)
+			continue
+		}
+
+		// check if we already have this evidence
+		if evpool.isPending(dve) {
+			evpool.logger.Debug("evidence already pending; ignoring", "evidence", dve)
+			continue
+		}
+
+		// check that the evidence is not already committed on chain
+		if evpool.isCommitted(dve) {
+			evpool.logger.Debug("evidence already committed; ignoring", "evidence", dve)
+			continue
+		}
+
+		if err := evpool.addPendingEvidence(dve); err != nil {
 			evpool.logger.Error("failed to flush evidence from consensus buffer to pending list: %w", err)
 			continue
 		}
 
-		evpool.evidenceList.PushBack(ev)
+		evpool.evidenceList.PushBack(dve)
+
+		evpool.logger.Info("verified new evidence of byzantine behavior", "evidence", dve)
 	}
 	// reset consensus buffer
-	evpool.consensusBuffer = make([]types.Evidence, 0)
+	evpool.consensusBuffer = make([]duplicateVoteSet, 0)
+}
+
+type duplicateVoteSet struct {
+	VoteA *types.Vote
+	VoteB *types.Vote
 }
 
 func bytesToEv(evBytes []byte) (types.Evidence, error) {
 	var evpb tmproto.Evidence
 	err := evpb.Unmarshal(evBytes)
diff --git a/evidence/pool_test.go b/evidence/pool_test.go
index 7d842003c..64c6f1b03 100644
--- a/evidence/pool_test.go
+++ b/evidence/pool_test.go
@@ -142,16 +142,21 @@ func TestAddExpiredEvidence(t *testing.T) {
 			}
 		})
 	}
 }
 
-func TestAddEvidenceFromConsensus(t *testing.T) {
+func TestReportConflictingVotes(t *testing.T) {
 	var height int64 = 10
-	pool, val := defaultTestPool(height)
-	ev := types.NewMockDuplicateVoteEvidenceWithValidator(height, defaultEvidenceTime, val, evidenceChainID)
 
-	require.NoError(t, pool.AddEvidenceFromConsensus(ev))
+	pool, pv := defaultTestPool(height)
+	val := types.NewValidator(pv.PrivKey.PubKey(), 10)
+	ev := types.NewMockDuplicateVoteEvidenceWithValidator(height+1, defaultEvidenceTime, pv, evidenceChainID)
+
+	pool.ReportConflictingVotes(ev.VoteA, ev.VoteB)
+
+	// shouldn't be able to submit the same evidence twice
+	pool.ReportConflictingVotes(ev.VoteA, ev.VoteB)
 
 	// evidence from consensus should not be added immediately but reside in the consensus buffer
 	evList, evSize := pool.PendingEvidence(defaultEvidenceMaxBytes)
 	require.Empty(t, evList)
 	require.Zero(t, evSize)
@@ -160,23 +165,17 @@ func TestAddEvidenceFromConsensus(t *testing.T) {
 	require.Nil(t, next)
 
 	// move to next height and update state and evidence pool
 	state := pool.State()
 	state.LastBlockHeight++
+	state.LastBlockTime = ev.Time()
+	state.LastValidators = types.NewValidatorSet([]*types.Validator{val})
 	pool.Update(state, []types.Evidence{})
 
 	// should be able to retrieve evidence from pool
 	evList, _ = pool.PendingEvidence(defaultEvidenceMaxBytes)
 	require.Equal(t, []types.Evidence{ev}, evList)
-
-	// shouldn't be able to submit the same evidence twice
-	require.NoError(t, pool.AddEvidenceFromConsensus(ev))
-	state = pool.State()
-	state.LastBlockHeight++
-	pool.Update(state, []types.Evidence{})
-	evList2, _ := pool.PendingEvidence(defaultEvidenceMaxBytes)
-	require.Equal(t, evList, evList2)
 }
 
 func TestEvidencePoolUpdate(t *testing.T) {
 	height := int64(21)
 	pool, val := defaultTestPool(height)
diff --git a/node/node_test.go b/node/node_test.go
index e94da4da5..8cbf38601 100644
--- a/node/node_test.go
+++ b/node/node_test.go
@@ -263,12 +263,11 @@ func TestCreateProposalBlock(t *testing.T) {
 	// than can fit in a block
 	var currentBytes int64 = 0
 	for currentBytes <= maxEvidenceBytes {
 		ev := types.NewMockDuplicateVoteEvidenceWithValidator(height, time.Now(), privVals[0], "test-chain")
 		currentBytes += int64(len(ev.Bytes()))
-		err := evidencePool.AddEvidenceFromConsensus(ev)
-		require.NoError(t, err)
+		evidencePool.ReportConflictingVotes(ev.VoteA, ev.VoteB)
 	}
 
 	evList, size := evidencePool.PendingEvidence(state.ConsensusParams.Evidence.MaxBytes)
 	require.Less(t, size, state.ConsensusParams.Evidence.MaxBytes+1)
 	evData := &types.EvidenceData{Evidence: evList}
diff --git a/state/services.go b/state/services.go
index 5213f8fdd..a46863904 100644
--- a/state/services.go
+++ b/state/services.go
@@ -51,11 +51,9 @@ type EvidencePool interface {
 type EmptyEvidencePool struct{}
 
 func (EmptyEvidencePool) PendingEvidence(maxBytes int64) (ev []types.Evidence, size int64) {
 	return nil, 0
 }
-func (EmptyEvidencePool) AddEvidence(types.Evidence) error              { return nil }
-func (EmptyEvidencePool) Update(State, types.EvidenceList)              {}
-func (EmptyEvidencePool) CheckEvidence(evList types.EvidenceList) error { return nil }
-func (EmptyEvidencePool) AddEvidenceFromConsensus(evidence types.Evidence) error {
-	return nil
-}
+func (EmptyEvidencePool) AddEvidence(types.Evidence) error                { return nil }
+func (EmptyEvidencePool) Update(State, types.EvidenceList)                {}
+func (EmptyEvidencePool) CheckEvidence(evList types.EvidenceList) error   { return nil }
+func (EmptyEvidencePool) ReportConflictingVotes(voteA, voteB *types.Vote) {}
diff --git a/test/maverick/consensus/state.go b/test/maverick/consensus/state.go
index a1385bd1e..bec89e1b1 100644
--- a/test/maverick/consensus/state.go
+++ b/test/maverick/consensus/state.go
@@ -463,13 +463,12 @@ type txNotifier interface {
 	TxsAvailable() <-chan struct{}
 }
 
 // interface to the evidence pool
 type evidencePool interface {
-	// Adds consensus based evidence to the evidence pool where time is the time
-	// of the block where the offense occurred and the validator set is the current one.
-	AddEvidenceFromConsensus(evidence types.Evidence) error
+	// reports conflicting votes to the evidence pool to be processed into evidence
+	ReportConflictingVotes(voteA, voteB *types.Vote)
 }
 
 //----------------------------------------
 // Public interface
 
@@ -1765,21 +1764,11 @@ func (cs *State) tryAddVote(vote *types.Vote, peerID p2p.ID) (bool, error) {
 					vote.Round,
 					"type",
 					vote.Type)
 				return added, err
 			}
-			var timestamp time.Time
-			if voteErr.VoteA.Height == cs.state.InitialHeight {
-				timestamp = cs.state.LastBlockTime // genesis time
-			} else {
-				timestamp = sm.MedianTime(cs.LastCommit.MakeCommit(), cs.LastValidators)
-			}
-			ev := types.NewDuplicateVoteEvidence(voteErr.VoteA, voteErr.VoteB, timestamp, cs.Validators)
-			evidenceErr := cs.evpool.AddEvidenceFromConsensus(ev)
-			if evidenceErr != nil {
-				cs.Logger.Error("Failed to add evidence to the evidence pool", "err", evidenceErr)
-			}
+			cs.evpool.ReportConflictingVotes(voteErr.VoteA, voteErr.VoteB)
 			return added, err
 		} else if err == types.ErrVoteNonDeterministicSignature {
 			cs.Logger.Debug("Vote has non-deterministic signature", "err", err)
 		} else {
 			// Either

Vulnerable code: 	// Adds consensus based evidence to the evidence pool. This function differs to
	// AddEvidence by bypassing verification and adding it immediately to the pool
	AddEvidenceFromConsensus(types.Evidence) error
			var timestamp time.Time
			if voteErr.VoteA.Height == cs.state.InitialHeight {
				timestamp = cs.state.LastBlockTime // genesis time
			} else {
				timestamp = sm.MedianTime(cs.LastCommit.MakeCommit(), cs.LastValidators)
			}
			// form duplicate vote evidence from the conflicting votes and send it across to the
			// evidence pool
			ev := types.NewDuplicateVoteEvidence(voteErr.VoteA, voteErr.VoteB, timestamp, cs.Validators)
			evidenceErr := cs.evpool.AddEvidenceFromConsensus(ev)
			if evidenceErr != nil {
				cs.Logger.Error("Failed to add evidence to the evidence pool", "err", evidenceErr)
			} else {
				cs.Logger.Debug("Added evidence to the evidence pool", "ev", ev)
			}
	// evidence from consensus if buffered to this slice, awaiting until the next height
	consensusBuffer []types.Evidence
		consensusBuffer: make([]types.Evidence, 0),
// Update pulls the latest state to be used for expiration and evidence params and then prunes all expired evidence
			"Failed EvidencePool.Update new state height is less than or equal to previous state height: %d <= %d",
	evpool.logger.Info("Updating evidence pool", "last_block_height", state.LastBlockHeight,
	evpool.logger.Info(
		"updating evidence pool",
		"last_block_height", state.LastBlockHeight,
		"last_block_time", state.LastBlockTime,
	)

	evpool.mtx.Lock()
	// flush awaiting evidence from consensus into pool
	evpool.flushConsensusBuffer()
	evpool.state = state
	evpool.mtx.Unlock()
		evpool.logger.Info("Evidence already pending, ignoring this one", "ev", ev)
// AddEvidenceFromConsensus should be exposed only to the consensus reactor so it can add evidence
// to the pool directly without the need for verification.
func (evpool *Pool) AddEvidenceFromConsensus(ev types.Evidence) error {

	// we already have this evidence, log this but don't return an error.
	if evpool.isPending(ev) {
		evpool.logger.Info("Evidence already pending, ignoring this one", "ev", ev)
		return nil
	}

	// add evidence to a buffer which will pass the evidence to the pool at the following height.
	// This avoids the issue of some nodes verifying and proposing evidence at a height where the
	// block hasn't been committed on cause others to potentially fail.
	evpool.consensusBuffer = append(evpool.consensusBuffer, ev)
	evpool.logger.Info("received new evidence of byzantine behavior from consensus", "evidence", ev)

	return nil
				return &types.ErrInvalidEvidence{Evidence: ev, Reason: err}
		evpool.logger.Info("Deleted pending evidence", "evidence", evidence)
// flushConsensusBuffer moves the evidence produced from consensus into the evidence pool
// and list so that it can be broadcasted and proposed
func (evpool *Pool) flushConsensusBuffer() {
	for _, ev := range evpool.consensusBuffer {
		if err := evpool.addPendingEvidence(ev); err != nil {
		evpool.evidenceList.PushBack(ev)
	evpool.consensusBuffer = make([]types.Evidence, 0)
func TestAddEvidenceFromConsensus(t *testing.T) {
	pool, val := defaultTestPool(height)
	ev := types.NewMockDuplicateVoteEvidenceWithValidator(height, defaultEvidenceTime, val, evidenceChainID)
	require.NoError(t, pool.AddEvidenceFromConsensus(ev))

	// shouldn't be able to submit the same evidence twice
	require.NoError(t, pool.AddEvidenceFromConsensus(ev))
	state = pool.State()
	state.LastBlockHeight++
	pool.Update(state, []types.Evidence{})
	evList2, _ := pool.PendingEvidence(defaultEvidenceMaxBytes)
	require.Equal(t, evList, evList2)
		err := evidencePool.AddEvidenceFromConsensus(ev)
		require.NoError(t, err)
func (EmptyEvidencePool) AddEvidence(types.Evidence) error              { return nil }
func (EmptyEvidencePool) Update(State, types.EvidenceList)              {}
func (EmptyEvidencePool) CheckEvidence(evList types.EvidenceList) error { return nil }
func (EmptyEvidencePool) AddEvidenceFromConsensus(evidence types.Evidence) error {
	return nil
}
	// Adds consensus based evidence to the evidence pool where time is the time
	// of the block where the offense occurred and the validator set is the current one.
	AddEvidenceFromConsensus(evidence types.Evidence) error
			var timestamp time.Time
			if voteErr.VoteA.Height == cs.state.InitialHeight {
				timestamp = cs.state.LastBlockTime // genesis time
			} else {
				timestamp = sm.MedianTime(cs.LastCommit.MakeCommit(), cs.LastValidators)
			}
			ev := types.NewDuplicateVoteEvidence(voteErr.VoteA, voteErr.VoteB, timestamp, cs.Validators)
			evidenceErr := cs.evpool.AddEvidenceFromConsensus(ev)
			if evidenceErr != nil {
				cs.Logger.Error("Failed to add evidence to the evidence pool", "err", evidenceErr)
			}
Fixed code: 	// 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 := css[1]

	lazyProposer.decideProposal = func(height int64, round int32) {
		lazyProposer.Logger.Info("Lazy Proposer proposing condensed commit")
		if lazyProposer.privValidator == nil {
			panic("entered createProposalBlock with privValidator being nil")
		}

		var commit *types.Commit
		switch {
		case lazyProposer.Height == lazyProposer.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 lazyProposer.LastCommit.HasTwoThirdsMajority():
			// Make the commit from LastCommit
			commit = lazyProposer.LastCommit.MakeCommit()
		default: // This shouldn't happen.
			lazyProposer.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 lazyProposer.privValidatorPubKey == nil {
			// If this node is a validator & proposer in the current round, it will
			// miss the opportunity to create a block.
			lazyProposer.Logger.Error(fmt.Sprintf("enterPropose: %v", errPubKeyIsNotSet))
			return
		}
		proposerAddr := lazyProposer.privValidatorPubKey.Address()

		block, blockParts := lazyProposer.blockExec.CreateProposalBlock(
			lazyProposer.Height, lazyProposer.state, commit, proposerAddr,
		)

		// Flush the WAL. Otherwise, we may not recompute the same proposal to sign,
		// and the privValidator will refuse to sign anything.
		if err := lazyProposer.wal.FlushAndSync(); err != nil {
			lazyProposer.Logger.Error("Error flushing to disk")
		}

		// Make proposal
		propBlockID := types.BlockID{Hash: block.Hash(), PartSetHeader: blockParts.Header()}
		proposal := types.NewProposal(height, round, lazyProposer.ValidRound, propBlockID)
		p := proposal.ToProto()
		if err := lazyProposer.privValidator.SignProposal(lazyProposer.state.ChainID, p); err == nil {
			proposal.Signature = p.Signature

			// send proposal and block parts on internal msg queue
			lazyProposer.sendInternalMessage(msgInfo{&ProposalMessage{proposal}, ""})
			for i := 0; i < int(blockParts.Total()); i++ {
				part := blockParts.GetPart(i)
				lazyProposer.sendInternalMessage(msgInfo{&BlockPartMessage{lazyProposer.Height, lazyProposer.Round, part}, ""})
			}
			lazyProposer.Logger.Info("Signed proposal", "height", height, "round", round, "proposal", proposal)
			lazyProposer.Logger.Debug(fmt.Sprintf("Signed proposal block: %v", block))
		} else if !lazyProposer.replayMode {
			lazyProposer.Logger.Error("enterPropose: Error signing proposal", "height", height, "round", round, "err", err)
		}
	}

	// reports conflicting votes to the evidence pool to be processed into evidence
	ReportConflictingVotes(voteA, voteB *types.Vote)
			// report conflicting votes to the evidence pool
			cs.evpool.ReportConflictingVotes(voteErr.VoteA, voteErr.VoteB)
			cs.Logger.Info("Found and sent conflicting votes to the evidence pool",
				"VoteA", voteErr.VoteA,
				"VoteB", voteErr.VoteB,
			)
	// evidence from consensus is buffered to this slice, awaiting until the next height
	consensusBuffer []duplicateVoteSet
		consensusBuffer: make([]duplicateVoteSet, 0),
// Update takes both the new state and the evidence committed at that height and performs
// the following operations:
// 1. Take any conflicting votes from consensus and use the state's LastBlockTime to form
//    DuplicateVoteEvidence and add it to the pool.
// 2. Update the pool's state which contains evidence params relating to expiry.
// 3. Moves pending evidence that has now been committed into the committed pool.
// 4. Removes any expired evidence based on both height and time.
			"failed EvidencePool.Update new state height is less than or equal to previous state height: %d <= %d",
	evpool.logger.Debug("Updating evidence pool", "last_block_height", state.LastBlockHeight,
	// flush conflicting vote pairs from the buffer, producing DuplicateVoteEvidence and
	// adding it to the pool
	evpool.processConsensusBuffer(state)
	evpool.updateState(state)
		evpool.logger.Debug("Evidence already pending, ignoring this one", "ev", ev)
// ReportConflictingVotes takes two conflicting votes and forms duplicate vote evidence,
// adding it eventually to the evidence pool.
//
// Duplicate vote attacks happen before the block is committed and the timestamp is
// finalized, thus the evidence pool holds these votes in a buffer, forming the
// evidence from them once consensus at that height has been reached and `Update()` with
// the new state called.
//
// Votes are not verified.
func (evpool *Pool) ReportConflictingVotes(voteA, voteB *types.Vote) {
	evpool.consensusBuffer = append(evpool.consensusBuffer, duplicateVoteSet{
		VoteA: voteA,
		VoteB: voteB,
	})
				return err
		evpool.logger.Debug("Deleted pending evidence", "evidence", evidence)
func (evpool *Pool) updateState(state sm.State) {
	evpool.mtx.Lock()
	defer evpool.mtx.Unlock()
	evpool.state = state
}

// processConsensusBuffer converts all the duplicate votes witnessed from consensus
// into DuplicateVoteEvidence. It sets the evidence timestamp to the block height
// from the most recently committed block.
// Evidence is then added to the pool so as to be ready to be broadcasted and proposed.
func (evpool *Pool) processConsensusBuffer(state sm.State) {
	evpool.mtx.Lock()
	defer evpool.mtx.Unlock()
	for _, voteSet := range evpool.consensusBuffer {

		// Check the height of the conflicting votes and fetch the corresponding time and validator set
		// to produce the valid evidence
		var dve *types.DuplicateVoteEvidence
		switch {
		case voteSet.VoteA.Height == state.LastBlockHeight:
			dve = types.NewDuplicateVoteEvidence(
				voteSet.VoteA,
				voteSet.VoteB,
				state.LastBlockTime,
				state.LastValidators,
			)

		case voteSet.VoteA.Height < state.LastBlockHeight:
			valSet, err := evpool.stateDB.LoadValidators(voteSet.VoteA.Height)
			if err != nil {
				evpool.logger.Error("failed to load validator set for conflicting votes", "height",
					voteSet.VoteA.Height, "err", err,
				)
				continue
			}
			blockMeta := evpool.blockStore.LoadBlockMeta(voteSet.VoteA.Height)
			if blockMeta == nil {
				evpool.logger.Error("failed to load block time for conflicting votes", "height", voteSet.VoteA.Height)
				continue
			}
			dve = types.NewDuplicateVoteEvidence(
				voteSet.VoteA,
				voteSet.VoteB,
				blockMeta.Header.Time,
				valSet,
			)

		default:
			// evidence pool shouldn't expect to get votes from consensus of a height that is above the current
			// state. If this error is seen then perhaps consider keeping the votes in the buffer and retry
			// in following heights
			evpool.logger.Error("inbound duplicate votes from consensus are of a greater height than current state",
				"duplicate vote height", voteSet.VoteA.Height,
				"state.LastBlockHeight", state.LastBlockHeight)
			continue
		}

		// check if we already have this evidence
		if evpool.isPending(dve) {
			evpool.logger.Debug("evidence already pending; ignoring", "evidence", dve)
			continue
		}

		// check that the evidence is not already committed on chain
		if evpool.isCommitted(dve) {
			evpool.logger.Debug("evidence already committed; ignoring", "evidence", dve)
			continue
		}

		if err := evpool.addPendingEvidence(dve); err != nil {
		evpool.evidenceList.PushBack(dve)

		evpool.logger.Info("verified new evidence of byzantine behavior", "evidence", dve)
	evpool.consensusBuffer = make([]duplicateVoteSet, 0)
}

type duplicateVoteSet struct {
	VoteA *types.Vote
	VoteB *types.Vote
func TestReportConflictingVotes(t *testing.T) {
	pool, pv := defaultTestPool(height)
	val := types.NewValidator(pv.PrivKey.PubKey(), 10)
	ev := types.NewMockDuplicateVoteEvidenceWithValidator(height+1, defaultEvidenceTime, pv, evidenceChainID)

	pool.ReportConflictingVotes(ev.VoteA, ev.VoteB)

	// shouldn't be able to submit the same evidence twice
	pool.ReportConflictingVotes(ev.VoteA, ev.VoteB)
	state.LastBlockTime = ev.Time()
	state.LastValidators = types.NewValidatorSet([]*types.Validator{val})
		evidencePool.ReportConflictingVotes(ev.VoteA, ev.VoteB)
func (EmptyEvidencePool) AddEvidence(types.Evidence) error                { return nil }
func (EmptyEvidencePool) Update(State, types.EvidenceList)                {}
func (EmptyEvidencePool) CheckEvidence(evList types.EvidenceList) error   { return nil }
func (EmptyEvidencePool) ReportConflictingVotes(voteA, voteB *types.Vote) {}
	// reports conflicting votes to the evidence pool to be processed into evidence
	ReportConflictingVotes(voteA, voteB *types.Vote)
			cs.evpool.ReportConflictingVotes(voteErr.VoteA, voteErr.VoteB)