mirror of
https://github.com/tendermint/tendermint.git
synced 2026-07-24 00:52:39 +00:00
Closes: #4530 This PR contains logic for both submitting an evidence by the light client (lite2 package) and receiving it on the Tendermint side (/broadcast_evidence RPC and/or EvidenceReactor#Receive). Upon receiving the ConflictingHeadersEvidence (introduced by this PR), the Tendermint validates it, then breaks it down into smaller pieces (DuplicateVoteEvidence, LunaticValidatorEvidence, PhantomValidatorEvidence, PotentialAmnesiaEvidence). Afterwards, each piece of evidence is verified against the state of the full node and added to the pool, from which it's reaped upon block creation. * rpc/client: do not pass height param if height ptr is nil * rpc/core: validate incoming evidence! * only accept ConflictingHeadersEvidence if one of the headers is committed from this full node's perspective This simplifies the code. Plus, if there are multiple forks, we'll likely to receive multiple ConflictingHeadersEvidence anyway. * swap CommitSig with Vote in LunaticValidatorEvidence Vote is needed to validate signature * no need to embed client http is a provider and should not be used as a client
327 lines
9.3 KiB
Go
327 lines
9.3 KiB
Go
package evidence
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
dbm "github.com/tendermint/tm-db"
|
|
|
|
clist "github.com/tendermint/tendermint/libs/clist"
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
sm "github.com/tendermint/tendermint/state"
|
|
"github.com/tendermint/tendermint/store"
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
// Pool maintains a pool of valid evidence in an Store.
|
|
type Pool struct {
|
|
logger log.Logger
|
|
|
|
store *Store
|
|
evidenceList *clist.CList // concurrent linked-list of evidence
|
|
|
|
// needed to load validators to verify evidence
|
|
stateDB dbm.DB
|
|
blockStore *store.BlockStore
|
|
|
|
// a map of active validators and respective last heights validator is active
|
|
// if it was in validator set after EvidenceParams.MaxAgeNumBlocks or
|
|
// currently is (ie. [MaxAgeNumBlocks, CurrentHeight])
|
|
// In simple words, it means it's still bonded -> therefore slashable.
|
|
valToLastHeight valToLastHeightMap
|
|
|
|
// latest state
|
|
mtx sync.Mutex
|
|
state sm.State
|
|
}
|
|
|
|
// Validator.Address -> Last height it was in validator set
|
|
type valToLastHeightMap map[string]int64
|
|
|
|
func NewPool(stateDB, evidenceDB dbm.DB, blockStore *store.BlockStore) (*Pool, error) {
|
|
var (
|
|
store = NewStore(evidenceDB)
|
|
state = sm.LoadState(stateDB)
|
|
)
|
|
|
|
valToLastHeight, err := buildValToLastHeightMap(state, stateDB, blockStore)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Pool{
|
|
stateDB: stateDB,
|
|
blockStore: blockStore,
|
|
state: state,
|
|
logger: log.NewNopLogger(),
|
|
store: store,
|
|
evidenceList: clist.New(),
|
|
valToLastHeight: valToLastHeight,
|
|
}, nil
|
|
}
|
|
|
|
func (evpool *Pool) EvidenceFront() *clist.CElement {
|
|
return evpool.evidenceList.Front()
|
|
}
|
|
|
|
func (evpool *Pool) EvidenceWaitChan() <-chan struct{} {
|
|
return evpool.evidenceList.WaitChan()
|
|
}
|
|
|
|
// SetLogger sets the Logger.
|
|
func (evpool *Pool) SetLogger(l log.Logger) {
|
|
evpool.logger = l
|
|
}
|
|
|
|
// PriorityEvidence returns the priority evidence.
|
|
func (evpool *Pool) PriorityEvidence() []types.Evidence {
|
|
return evpool.store.PriorityEvidence()
|
|
}
|
|
|
|
// PendingEvidence returns up to maxNum uncommitted evidence.
|
|
// If maxNum is -1, all evidence is returned.
|
|
func (evpool *Pool) PendingEvidence(maxNum int64) []types.Evidence {
|
|
return evpool.store.PendingEvidence(maxNum)
|
|
}
|
|
|
|
// State returns the current state of the evpool.
|
|
func (evpool *Pool) State() sm.State {
|
|
evpool.mtx.Lock()
|
|
defer evpool.mtx.Unlock()
|
|
return evpool.state
|
|
}
|
|
|
|
// Update loads the latest
|
|
func (evpool *Pool) Update(block *types.Block, state sm.State) {
|
|
// sanity check
|
|
if state.LastBlockHeight != block.Height {
|
|
panic(
|
|
fmt.Sprintf("Failed EvidencePool.Update sanity check: got state.Height=%d with block.Height=%d",
|
|
state.LastBlockHeight,
|
|
block.Height,
|
|
),
|
|
)
|
|
}
|
|
|
|
// update the state
|
|
evpool.mtx.Lock()
|
|
evpool.state = state
|
|
evpool.mtx.Unlock()
|
|
|
|
// remove evidence from pending and mark committed
|
|
evpool.MarkEvidenceAsCommitted(block.Height, block.Time, block.Evidence.Evidence)
|
|
|
|
evpool.updateValToLastHeight(block.Height, state)
|
|
}
|
|
|
|
// AddEvidence checks the evidence is valid and adds it to the pool. If
|
|
// evidence is composite (ConflictingHeadersEvidence), it will be broken up
|
|
// into smaller pieces.
|
|
func (evpool *Pool) AddEvidence(evidence types.Evidence) error {
|
|
var (
|
|
state = evpool.State()
|
|
evList = []types.Evidence{evidence}
|
|
)
|
|
|
|
valSet, err := sm.LoadValidators(evpool.stateDB, evidence.Height())
|
|
if err != nil {
|
|
return fmt.Errorf("can't load validators at height #%d: %w", evidence.Height(), err)
|
|
}
|
|
|
|
// Break composite evidence into smaller pieces.
|
|
if ce, ok := evidence.(types.CompositeEvidence); ok {
|
|
evpool.logger.Info("Breaking up composite evidence", "ev", evidence)
|
|
|
|
blockMeta := evpool.blockStore.LoadBlockMeta(evidence.Height())
|
|
if blockMeta == nil {
|
|
return fmt.Errorf("don't have block meta at height #%d", evidence.Height())
|
|
}
|
|
|
|
if err := ce.VerifyComposite(&blockMeta.Header, valSet); err != nil {
|
|
return err
|
|
}
|
|
|
|
evList = ce.Split(&blockMeta.Header, valSet, evpool.valToLastHeight)
|
|
}
|
|
|
|
for _, ev := range evList {
|
|
if evpool.store.Has(evidence) {
|
|
return ErrEvidenceAlreadyStored{}
|
|
}
|
|
|
|
// For lunatic validator evidence, a header needs to be fetched.
|
|
var header *types.Header
|
|
if _, ok := ev.(*types.LunaticValidatorEvidence); ok {
|
|
blockMeta := evpool.blockStore.LoadBlockMeta(ev.Height())
|
|
if blockMeta == nil {
|
|
return fmt.Errorf("don't have block meta at height #%d", ev.Height())
|
|
}
|
|
header = &blockMeta.Header
|
|
}
|
|
|
|
// 1) Verify against state.
|
|
if err := sm.VerifyEvidence(evpool.stateDB, state, ev, header); err != nil {
|
|
return fmt.Errorf("failed to verify %v: %w", ev, err)
|
|
}
|
|
|
|
// 2) Compute priority.
|
|
_, val := valSet.GetByAddress(ev.Address())
|
|
priority := val.VotingPower
|
|
|
|
// 3) Save to store.
|
|
_, err := evpool.store.AddNewEvidence(ev, priority)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add new evidence %v: %w", ev, err)
|
|
}
|
|
|
|
// 4) Add evidence to clist.
|
|
evpool.evidenceList.PushBack(ev)
|
|
|
|
evpool.logger.Info("Verified new evidence of byzantine behaviour", "evidence", ev)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MarkEvidenceAsCommitted marks all the evidence as committed and removes it
|
|
// from the queue.
|
|
func (evpool *Pool) MarkEvidenceAsCommitted(height int64, lastBlockTime time.Time, evidence []types.Evidence) {
|
|
// make a map of committed evidence to remove from the clist
|
|
blockEvidenceMap := make(map[string]struct{})
|
|
for _, ev := range evidence {
|
|
evpool.store.MarkEvidenceAsCommitted(ev)
|
|
blockEvidenceMap[evMapKey(ev)] = struct{}{}
|
|
}
|
|
|
|
// remove committed evidence from the clist
|
|
evidenceParams := evpool.State().ConsensusParams.Evidence
|
|
evpool.removeEvidence(height, lastBlockTime, evidenceParams, blockEvidenceMap)
|
|
}
|
|
|
|
// IsCommitted returns true if we have already seen this exact evidence and it
|
|
// is already marked as committed.
|
|
func (evpool *Pool) IsCommitted(evidence types.Evidence) bool {
|
|
ei := evpool.store.getInfo(evidence)
|
|
return ei.Evidence != nil && ei.Committed
|
|
}
|
|
|
|
// ValidatorLastHeight returns the last height of the validator w/ the
|
|
// given address. 0 - if address never was a validator or was such a
|
|
// long time ago (> ConsensusParams.Evidence.MaxAgeDuration && >
|
|
// ConsensusParams.Evidence.MaxAgeNumBlocks).
|
|
func (evpool *Pool) ValidatorLastHeight(address []byte) int64 {
|
|
h, ok := evpool.valToLastHeight[string(address)]
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (evpool *Pool) removeEvidence(
|
|
height int64,
|
|
lastBlockTime time.Time,
|
|
params types.EvidenceParams,
|
|
blockEvidenceMap map[string]struct{}) {
|
|
|
|
for e := evpool.evidenceList.Front(); e != nil; e = e.Next() {
|
|
var (
|
|
ev = e.Value.(types.Evidence)
|
|
ageDuration = lastBlockTime.Sub(ev.Time())
|
|
ageNumBlocks = height - ev.Height()
|
|
)
|
|
|
|
// Remove the evidence if it's already in a block or if it's now too old.
|
|
if _, ok := blockEvidenceMap[evMapKey(ev)]; ok ||
|
|
(ageDuration > params.MaxAgeDuration && ageNumBlocks > params.MaxAgeNumBlocks) {
|
|
// remove from clist
|
|
evpool.evidenceList.Remove(e)
|
|
e.DetachPrev()
|
|
}
|
|
}
|
|
}
|
|
|
|
func evMapKey(ev types.Evidence) string {
|
|
return string(ev.Hash())
|
|
}
|
|
|
|
func (evpool *Pool) updateValToLastHeight(blockHeight int64, state sm.State) {
|
|
// Update current validators & add new ones.
|
|
for _, val := range state.Validators.Validators {
|
|
evpool.valToLastHeight[string(val.Address)] = blockHeight
|
|
}
|
|
|
|
// Remove validators outside of MaxAgeNumBlocks & MaxAgeDuration.
|
|
removeHeight := blockHeight - evpool.State().ConsensusParams.Evidence.MaxAgeNumBlocks
|
|
if removeHeight >= 1 {
|
|
valSet, err := sm.LoadValidators(evpool.stateDB, removeHeight)
|
|
if err != nil {
|
|
for _, val := range valSet.Validators {
|
|
h, ok := evpool.valToLastHeight[string(val.Address)]
|
|
if ok && h == removeHeight {
|
|
delete(evpool.valToLastHeight, string(val.Address))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildValToLastHeightMap(state sm.State, stateDB dbm.DB, blockStore *store.BlockStore) (valToLastHeightMap, error) {
|
|
var (
|
|
valToLastHeight = make(map[string]int64)
|
|
params = state.ConsensusParams.Evidence
|
|
|
|
numBlocks = int64(0)
|
|
minAgeTime = time.Now().Add(-params.MaxAgeDuration)
|
|
height = state.LastBlockHeight
|
|
)
|
|
|
|
if height == 0 {
|
|
return valToLastHeight, nil
|
|
}
|
|
|
|
meta := blockStore.LoadBlockMeta(height)
|
|
if meta == nil {
|
|
return nil, fmt.Errorf("block meta for height %d not found", height)
|
|
}
|
|
blockTime := meta.Header.Time
|
|
|
|
// From state.LastBlockHeight, build a map of "active" validators until
|
|
// MaxAgeNumBlocks is passed and block time is less than now() -
|
|
// MaxAgeDuration.
|
|
for height >= 1 && (numBlocks <= params.MaxAgeNumBlocks || !blockTime.Before(minAgeTime)) {
|
|
valSet, err := sm.LoadValidators(stateDB, height)
|
|
if err != nil {
|
|
// last stored height -> return
|
|
if _, ok := err.(sm.ErrNoValSetForHeight); ok {
|
|
return valToLastHeight, nil
|
|
}
|
|
return nil, fmt.Errorf("validator set for height %d not found", height)
|
|
}
|
|
|
|
for _, val := range valSet.Validators {
|
|
key := string(val.Address)
|
|
if _, ok := valToLastHeight[key]; !ok {
|
|
valToLastHeight[key] = height
|
|
}
|
|
}
|
|
|
|
height--
|
|
|
|
if height > 0 {
|
|
// NOTE: we assume here blockStore and state.Validators are in sync. I.e if
|
|
// block N is stored, then validators for height N are also stored in
|
|
// state.
|
|
meta := blockStore.LoadBlockMeta(height)
|
|
if meta == nil {
|
|
return nil, fmt.Errorf("block meta for height %d not found", height)
|
|
}
|
|
blockTime = meta.Header.Time
|
|
}
|
|
|
|
numBlocks++
|
|
}
|
|
|
|
return valToLastHeight, nil
|
|
}
|