mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-05 13:05:09 +00:00
evidence: structs can independently form abci evidence (#5610)
This commit is contained in:
342
evidence/pool.go
342
evidence/pool.go
@@ -4,7 +4,7 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -13,10 +13,8 @@ import (
|
||||
gogotypes "github.com/gogo/protobuf/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
clist "github.com/tendermint/tendermint/libs/clist"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
evproto "github.com/tendermint/tendermint/proto/tendermint/evidence"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
@@ -94,7 +92,7 @@ func (evpool *Pool) PendingEvidence(maxBytes int64) ([]types.Evidence, int64) {
|
||||
}
|
||||
|
||||
// Update pulls the latest state to be used for expiration and evidence params and then prunes all expired evidence
|
||||
func (evpool *Pool) Update(state sm.State) {
|
||||
func (evpool *Pool) Update(state sm.State, ev types.EvidenceList) {
|
||||
// sanity check
|
||||
if state.LastBlockHeight <= evpool.state.LastBlockHeight {
|
||||
panic(fmt.Sprintf(
|
||||
@@ -109,6 +107,8 @@ func (evpool *Pool) Update(state sm.State) {
|
||||
// update the state
|
||||
evpool.updateState(state)
|
||||
|
||||
evpool.markEvidenceAsCommitted(ev)
|
||||
|
||||
// prune pending evidence when it has expired. This also updates when the next evidence will expire
|
||||
if evpool.Size() > 0 && state.LastBlockHeight > evpool.pruningHeight &&
|
||||
state.LastBlockTime.After(evpool.pruningTime) {
|
||||
@@ -135,13 +135,13 @@ func (evpool *Pool) AddEvidence(ev types.Evidence) error {
|
||||
}
|
||||
|
||||
// 1) Verify against state.
|
||||
evInfo, err := evpool.verify(ev)
|
||||
err := evpool.verify(ev)
|
||||
if err != nil {
|
||||
return types.NewErrInvalidEvidence(ev, err)
|
||||
}
|
||||
|
||||
// 2) Save to store.
|
||||
if err := evpool.addPendingEvidence(evInfo); err != nil {
|
||||
if err := evpool.addPendingEvidence(ev); err != nil {
|
||||
return fmt.Errorf("can't add evidence to pending list: %w", err)
|
||||
}
|
||||
|
||||
@@ -153,13 +153,9 @@ func (evpool *Pool) AddEvidence(ev types.Evidence) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddEvidenceFromConsensus should be exposed only to the consensus so it can add evidence to the pool
|
||||
// directly without the need for verification.
|
||||
func (evpool *Pool) AddEvidenceFromConsensus(ev types.Evidence, time time.Time, valSet *types.ValidatorSet) error {
|
||||
var (
|
||||
vals []*types.Validator
|
||||
totalPower int64
|
||||
)
|
||||
// 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) {
|
||||
@@ -167,23 +163,7 @@ func (evpool *Pool) AddEvidenceFromConsensus(ev types.Evidence, time time.Time,
|
||||
return nil
|
||||
}
|
||||
|
||||
switch ev := ev.(type) {
|
||||
case *types.DuplicateVoteEvidence:
|
||||
_, val := valSet.GetByAddress(ev.VoteA.ValidatorAddress)
|
||||
vals = append(vals, val)
|
||||
totalPower = valSet.TotalVotingPower()
|
||||
default:
|
||||
return fmt.Errorf("unrecognized evidence type: %T", ev)
|
||||
}
|
||||
|
||||
evInfo := &info{
|
||||
Evidence: ev,
|
||||
Time: time,
|
||||
Validators: vals,
|
||||
TotalVotingPower: totalPower,
|
||||
}
|
||||
|
||||
if err := evpool.addPendingEvidence(evInfo); err != nil {
|
||||
if err := evpool.addPendingEvidence(ev); err != nil {
|
||||
return fmt.Errorf("can't add evidence to pending list: %w", err)
|
||||
}
|
||||
// add evidence to be gossiped with peers
|
||||
@@ -210,15 +190,15 @@ func (evpool *Pool) CheckEvidence(evList types.EvidenceList) error {
|
||||
return &types.ErrInvalidEvidence{Evidence: ev, Reason: errors.New("evidence was already committed")}
|
||||
}
|
||||
|
||||
evInfo, err := evpool.verify(ev)
|
||||
err := evpool.verify(ev)
|
||||
if err != nil {
|
||||
return &types.ErrInvalidEvidence{Evidence: ev, Reason: err}
|
||||
}
|
||||
|
||||
if err := evpool.addPendingEvidence(evInfo); err != nil {
|
||||
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
|
||||
evpool.logger.Error("Can't add evidence to pending list", "err", err, "evInfo", evInfo)
|
||||
evpool.logger.Error("Can't add evidence to pending list", "err", err, "ev", ev)
|
||||
}
|
||||
|
||||
evpool.logger.Info("Verified new evidence of byzantine behavior", "evidence", ev)
|
||||
@@ -236,85 +216,6 @@ func (evpool *Pool) CheckEvidence(evList types.EvidenceList) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ABCIEvidence processes all the evidence in the block, marking it as committed and removing it
|
||||
// from the pending database. It then forms the individual abci evidence that will be passed back to
|
||||
// the application.
|
||||
func (evpool *Pool) ABCIEvidence(height int64, evidence []types.Evidence) []abci.Evidence {
|
||||
// make a map of committed evidence to remove from the clist
|
||||
blockEvidenceMap := make(map[string]struct{}, len(evidence))
|
||||
abciEvidence := make([]abci.Evidence, 0)
|
||||
for _, ev := range evidence {
|
||||
|
||||
// get entire evidence info from pending list
|
||||
infoBytes, err := evpool.evidenceStore.Get(keyPending(ev))
|
||||
if err != nil {
|
||||
evpool.logger.Error("Unable to retrieve evidence to pass to ABCI. "+
|
||||
"Evidence pool should have seen this evidence before",
|
||||
"evidence", ev, "err", err)
|
||||
continue
|
||||
}
|
||||
var infoProto evproto.Info
|
||||
err = infoProto.Unmarshal(infoBytes)
|
||||
if err != nil {
|
||||
evpool.logger.Error("Decoding evidence info failed", "err", err, "height", ev.Height(), "hash", ev.Hash())
|
||||
continue
|
||||
}
|
||||
evInfo, err := infoFromProto(&infoProto)
|
||||
if err != nil {
|
||||
evpool.logger.Error("Converting evidence info from proto failed", "err", err, "height", ev.Height(),
|
||||
"hash", ev.Hash())
|
||||
continue
|
||||
}
|
||||
|
||||
var evType abci.EvidenceType
|
||||
switch ev.(type) {
|
||||
case *types.DuplicateVoteEvidence:
|
||||
evType = abci.EvidenceType_DUPLICATE_VOTE
|
||||
case *types.LightClientAttackEvidence:
|
||||
evType = abci.EvidenceType_LIGHT_CLIENT_ATTACK
|
||||
default:
|
||||
evpool.logger.Error("Unknown evidence type", "T", reflect.TypeOf(ev))
|
||||
continue
|
||||
}
|
||||
for _, val := range evInfo.Validators {
|
||||
abciEv := abci.Evidence{
|
||||
Type: evType,
|
||||
Validator: types.TM2PB.Validator(val),
|
||||
Height: ev.Height(),
|
||||
Time: evInfo.Time,
|
||||
TotalVotingPower: evInfo.TotalVotingPower,
|
||||
}
|
||||
abciEvidence = append(abciEvidence, abciEv)
|
||||
evpool.logger.Info("Created ABCI evidence", "ev", abciEv)
|
||||
}
|
||||
|
||||
// we can now remove the evidence from the pending list and the clist that we use for gossiping
|
||||
evpool.removePendingEvidence(ev)
|
||||
blockEvidenceMap[evMapKey(ev)] = struct{}{}
|
||||
|
||||
// Add evidence to the committed list
|
||||
// As the evidence is stored in the block store we only need to record the height that it was saved at.
|
||||
key := keyCommitted(ev)
|
||||
|
||||
h := gogotypes.Int64Value{Value: height}
|
||||
evBytes, err := proto.Marshal(&h)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := evpool.evidenceStore.Set(key, evBytes); err != nil {
|
||||
evpool.logger.Error("Unable to add committed evidence", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// remove committed evidence from the clist
|
||||
if len(blockEvidenceMap) != 0 {
|
||||
evpool.removeEvidenceFromList(blockEvidenceMap)
|
||||
}
|
||||
|
||||
return abciEvidence
|
||||
}
|
||||
|
||||
// EvidenceFront goes to the first evidence in the clist
|
||||
func (evpool *Pool) EvidenceFront() *clist.CElement {
|
||||
return evpool.evidenceList.Front()
|
||||
@@ -330,6 +231,7 @@ func (evpool *Pool) SetLogger(l log.Logger) {
|
||||
evpool.logger = l
|
||||
}
|
||||
|
||||
// Size returns the number of evidence in the pool.
|
||||
func (evpool *Pool) Size() uint32 {
|
||||
return atomic.LoadUint32(&evpool.evidenceSize)
|
||||
}
|
||||
@@ -343,106 +245,59 @@ func (evpool *Pool) State() sm.State {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// Info is a wrapper around the evidence that the evidence pool receives with extensive
|
||||
// information of what validators were malicious, the time of the attack and the total voting power
|
||||
// This is saved as a form of cache so that the evidence pool can easily produce the ABCI Evidence
|
||||
// needed to be sent to the application.
|
||||
type info struct {
|
||||
Evidence types.Evidence
|
||||
Time time.Time
|
||||
Validators []*types.Validator
|
||||
TotalVotingPower int64
|
||||
ByteSize int64
|
||||
}
|
||||
|
||||
// ToProto encodes into protobuf
|
||||
func (ei info) ToProto() (*evproto.Info, error) {
|
||||
evpb, err := types.EvidenceToProto(ei.Evidence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valsProto := make([]*tmproto.Validator, len(ei.Validators))
|
||||
for i := 0; i < len(ei.Validators); i++ {
|
||||
valp, err := ei.Validators[i].ToProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valsProto[i] = valp
|
||||
}
|
||||
|
||||
return &evproto.Info{
|
||||
Evidence: *evpb,
|
||||
Time: ei.Time,
|
||||
Validators: valsProto,
|
||||
TotalVotingPower: ei.TotalVotingPower,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InfoFromProto decodes from protobuf into Info
|
||||
func infoFromProto(proto *evproto.Info) (info, error) {
|
||||
if proto == nil {
|
||||
return info{}, errors.New("nil evidence info")
|
||||
}
|
||||
|
||||
ev, err := types.EvidenceFromProto(&proto.Evidence)
|
||||
if err != nil {
|
||||
return info{}, err
|
||||
}
|
||||
|
||||
vals := make([]*types.Validator, len(proto.Validators))
|
||||
for i := 0; i < len(proto.Validators); i++ {
|
||||
val, err := types.ValidatorFromProto(proto.Validators[i])
|
||||
if err != nil {
|
||||
return info{}, err
|
||||
}
|
||||
vals[i] = val
|
||||
}
|
||||
|
||||
return info{
|
||||
Evidence: ev,
|
||||
Time: proto.Time,
|
||||
Validators: vals,
|
||||
TotalVotingPower: proto.TotalVotingPower,
|
||||
ByteSize: int64(proto.Evidence.Size()),
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// fastCheck leverages the fact that the evidence pool may have already verified the evidence to see if it can
|
||||
// quickly conclude that the evidence is already valid.
|
||||
func (evpool *Pool) fastCheck(ev types.Evidence) bool {
|
||||
key := keyPending(ev)
|
||||
if lcae, ok := ev.(*types.LightClientAttackEvidence); ok {
|
||||
key := keyPending(ev)
|
||||
evBytes, err := evpool.evidenceStore.Get(key)
|
||||
if evBytes == nil { // the evidence is not in the nodes pending list
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
evpool.logger.Error("Failed to load evidence", "err", err, "evidence", lcae)
|
||||
evpool.logger.Error("Failed to load light client attack evidence", "err", err, "key(height/hash)", key)
|
||||
return false
|
||||
}
|
||||
evInfo, err := bytesToInfo(evBytes)
|
||||
var trustedPb tmproto.LightClientAttackEvidence
|
||||
err = trustedPb.Unmarshal(evBytes)
|
||||
if err != nil {
|
||||
evpool.logger.Error("Failed to convert evidence from proto", "err", err, "evidence", lcae)
|
||||
evpool.logger.Error("Failed to convert light client attack evidence from bytes",
|
||||
"err", err, "key(height/hash)", key)
|
||||
return false
|
||||
}
|
||||
// ensure that all the validators that the evidence pool have found to be malicious
|
||||
// are present in the list of commit signatures in the conflicting block
|
||||
OUTER:
|
||||
for _, sig := range lcae.ConflictingBlock.Commit.Signatures {
|
||||
for _, val := range evInfo.Validators {
|
||||
if bytes.Equal(val.Address, sig.ValidatorAddress) {
|
||||
continue OUTER
|
||||
}
|
||||
trustedEv, err := types.LightClientAttackEvidenceFromProto(&trustedPb)
|
||||
if err != nil {
|
||||
evpool.logger.Error("Failed to convert light client attack evidence from protobuf",
|
||||
"err", err, "key(height/hash)", key)
|
||||
return false
|
||||
}
|
||||
// ensure that all the byzantine validators that the evidence pool has match the byzantine validators
|
||||
// in this evidence
|
||||
if trustedEv.ByzantineValidators == nil && lcae.ByzantineValidators != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(trustedEv.ByzantineValidators) != len(lcae.ByzantineValidators) {
|
||||
return false
|
||||
}
|
||||
|
||||
byzValsCopy := make([]*types.Validator, len(lcae.ByzantineValidators))
|
||||
for i, v := range lcae.ByzantineValidators {
|
||||
byzValsCopy[i] = v.Copy()
|
||||
}
|
||||
|
||||
// ensure that both validator arrays are in the same order
|
||||
sort.Sort(types.ValidatorsByVotingPower(byzValsCopy))
|
||||
|
||||
for idx, val := range trustedEv.ByzantineValidators {
|
||||
if !bytes.Equal(byzValsCopy[idx].Address, val.Address) {
|
||||
return false
|
||||
}
|
||||
if byzValsCopy[idx].VotingPower != val.VotingPower {
|
||||
return false
|
||||
}
|
||||
// a validator we know is malicious is not included in the commit
|
||||
evpool.logger.Info("Fast check failed: a validator we know is malicious is not " +
|
||||
"in the commit sigs. Reverting to full verification")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -482,8 +337,8 @@ func (evpool *Pool) isPending(evidence types.Evidence) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func (evpool *Pool) addPendingEvidence(evInfo *info) error {
|
||||
evpb, err := evInfo.ToProto()
|
||||
func (evpool *Pool) addPendingEvidence(ev types.Evidence) error {
|
||||
evpb, err := types.EvidenceToProto(ev)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert to proto, err: %w", err)
|
||||
}
|
||||
@@ -493,7 +348,7 @@ func (evpool *Pool) addPendingEvidence(evInfo *info) error {
|
||||
return fmt.Errorf("unable to marshal evidence: %w", err)
|
||||
}
|
||||
|
||||
key := keyPending(evInfo.Evidence)
|
||||
key := keyPending(ev)
|
||||
|
||||
err = evpool.evidenceStore.Set(key, evBytes)
|
||||
if err != nil {
|
||||
@@ -513,31 +368,80 @@ func (evpool *Pool) removePendingEvidence(evidence types.Evidence) {
|
||||
}
|
||||
}
|
||||
|
||||
// markEvidenceAsCommitted processes all the evidence in the block, marking it as
|
||||
// committed and removing it from the pending database.
|
||||
func (evpool *Pool) markEvidenceAsCommitted(evidence types.EvidenceList) {
|
||||
blockEvidenceMap := make(map[string]struct{}, len(evidence))
|
||||
for _, ev := range evidence {
|
||||
if evpool.isPending(ev) {
|
||||
evpool.removePendingEvidence(ev)
|
||||
blockEvidenceMap[evMapKey(ev)] = struct{}{}
|
||||
}
|
||||
|
||||
// Add evidence to the committed list. As the evidence is stored in the block store
|
||||
// we only need to record the height that it was saved at.
|
||||
key := keyCommitted(ev)
|
||||
|
||||
h := gogotypes.Int64Value{Value: ev.Height()}
|
||||
evBytes, err := proto.Marshal(&h)
|
||||
if err != nil {
|
||||
evpool.logger.Error("failed to marshal committed evidence", "err", err, "key(height/hash)", key)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := evpool.evidenceStore.Set(key, evBytes); err != nil {
|
||||
evpool.logger.Error("Unable to save committed evidence", "err", err, "key(height/hash)", key)
|
||||
}
|
||||
}
|
||||
|
||||
// remove committed evidence from the clist
|
||||
if len(blockEvidenceMap) != 0 {
|
||||
evpool.removeEvidenceFromList(blockEvidenceMap)
|
||||
}
|
||||
}
|
||||
|
||||
// listEvidence retrieves lists evidence from oldest to newest within maxBytes.
|
||||
// If maxBytes is -1, there's no cap on the size of returned evidence.
|
||||
func (evpool *Pool) listEvidence(prefixKey byte, maxBytes int64) ([]types.Evidence, int64, error) {
|
||||
var totalSize int64
|
||||
var evidence []types.Evidence
|
||||
var (
|
||||
evSize int64
|
||||
totalSize int64
|
||||
evidence []types.Evidence
|
||||
evList tmproto.EvidenceList // used for calculating the bytes size
|
||||
)
|
||||
|
||||
iter, err := dbm.IteratePrefix(evpool.evidenceStore, []byte{prefixKey})
|
||||
if err != nil {
|
||||
return nil, totalSize, fmt.Errorf("database error: %v", err)
|
||||
}
|
||||
defer iter.Close()
|
||||
for ; iter.Valid(); iter.Next() {
|
||||
evInfo, err := bytesToInfo(iter.Value())
|
||||
var evpb tmproto.Evidence
|
||||
err := evpb.Unmarshal(iter.Value())
|
||||
if err != nil {
|
||||
return evidence, totalSize, err
|
||||
}
|
||||
evList.Evidence = append(evList.Evidence, evpb)
|
||||
evSize = int64(evList.Size())
|
||||
if maxBytes != -1 && evSize > maxBytes {
|
||||
if err := iter.Error(); err != nil {
|
||||
return evidence, totalSize, err
|
||||
}
|
||||
return evidence, totalSize, nil
|
||||
}
|
||||
|
||||
ev, err := types.EvidenceFromProto(&evpb)
|
||||
if err != nil {
|
||||
return nil, totalSize, err
|
||||
}
|
||||
|
||||
totalSize += evInfo.ByteSize
|
||||
|
||||
if maxBytes != -1 && totalSize > maxBytes {
|
||||
return evidence, totalSize - evInfo.ByteSize, nil
|
||||
}
|
||||
|
||||
evidence = append(evidence, evInfo.Evidence)
|
||||
totalSize = evSize
|
||||
evidence = append(evidence, ev)
|
||||
}
|
||||
|
||||
if err := iter.Error(); err != nil {
|
||||
return evidence, totalSize, err
|
||||
}
|
||||
return evidence, totalSize, nil
|
||||
}
|
||||
|
||||
@@ -550,22 +454,22 @@ func (evpool *Pool) removeExpiredPendingEvidence() (int64, time.Time) {
|
||||
defer iter.Close()
|
||||
blockEvidenceMap := make(map[string]struct{})
|
||||
for ; iter.Valid(); iter.Next() {
|
||||
evInfo, err := bytesToInfo(iter.Value())
|
||||
ev, err := bytesToEv(iter.Value())
|
||||
if err != nil {
|
||||
evpool.logger.Error("Error in transition evidence from protobuf", "err", err)
|
||||
continue
|
||||
}
|
||||
if !evpool.isExpired(evInfo.Evidence.Height(), evInfo.Time) {
|
||||
if !evpool.isExpired(ev.Height(), ev.Time()) {
|
||||
if len(blockEvidenceMap) != 0 {
|
||||
evpool.removeEvidenceFromList(blockEvidenceMap)
|
||||
}
|
||||
|
||||
// return the height and time with which this evidence will have expired so we know when to prune next
|
||||
return evInfo.Evidence.Height() + evpool.State().ConsensusParams.Evidence.MaxAgeNumBlocks + 1,
|
||||
evInfo.Time.Add(evpool.State().ConsensusParams.Evidence.MaxAgeDuration).Add(time.Second)
|
||||
return ev.Height() + evpool.State().ConsensusParams.Evidence.MaxAgeNumBlocks + 1,
|
||||
ev.Time().Add(evpool.State().ConsensusParams.Evidence.MaxAgeDuration).Add(time.Second)
|
||||
}
|
||||
evpool.removePendingEvidence(evInfo.Evidence)
|
||||
blockEvidenceMap[evMapKey(evInfo.Evidence)] = struct{}{}
|
||||
evpool.removePendingEvidence(ev)
|
||||
blockEvidenceMap[evMapKey(ev)] = struct{}{}
|
||||
}
|
||||
// We either have no pending evidence or all evidence has expired
|
||||
if len(blockEvidenceMap) != 0 {
|
||||
@@ -593,14 +497,14 @@ func (evpool *Pool) updateState(state sm.State) {
|
||||
evpool.state = state
|
||||
}
|
||||
|
||||
func bytesToInfo(evBytes []byte) (info, error) {
|
||||
var evpb evproto.Info
|
||||
func bytesToEv(evBytes []byte) (types.Evidence, error) {
|
||||
var evpb tmproto.Evidence
|
||||
err := evpb.Unmarshal(evBytes)
|
||||
if err != nil {
|
||||
return info{}, err
|
||||
return &types.DuplicateVoteEvidence{}, err
|
||||
}
|
||||
|
||||
return infoFromProto(&evpb)
|
||||
return types.EvidenceFromProto(&evpb)
|
||||
}
|
||||
|
||||
func evMapKey(ev types.Evidence) string {
|
||||
|
||||
Reference in New Issue
Block a user