|
package evidence
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/tendermint/tendermint/light"
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
// verify verifies the evidence fully by checking:
|
|
// - It has not already been committed
|
|
// - it is sufficiently recent (MaxAge)
|
|
// - it is from a key who was a validator at the given height
|
|
// - it is internally consistent with state
|
|
// - it was properly signed by the alleged equivocator and meets the individual evidence verification requirements
|
|
func (evpool *Pool) verify(evidence types.Evidence) (*info, error) {
|
|
var (
|
|
state = evpool.State()
|
|
height = state.LastBlockHeight
|
|
evidenceParams = state.ConsensusParams.Evidence
|
|
ageNumBlocks = height - evidence.Height()
|
|
)
|
|
|
|
// verify the time of the evidence
|
|
blockMeta := evpool.blockStore.LoadBlockMeta(evidence.Height())
|
|
if blockMeta == nil {
|
|
return nil, fmt.Errorf("don't have header at height #%d", evidence.Height())
|
|
}
|
|
evTime := blockMeta.Header.Time
|
|
ageDuration := state.LastBlockTime.Sub(evTime)
|
|
|
|
// check that the evidence hasn't expired
|
|
if ageDuration > evidenceParams.MaxAgeDuration && ageNumBlocks > evidenceParams.MaxAgeNumBlocks {
|
|
return nil, fmt.Errorf(
|
|
"evidence from height %d (created at: %v) is too old; min height is %d and evidence can not be older than %v",
|
|
evidence.Height(),
|
|
evTime,
|
|
height-evidenceParams.MaxAgeNumBlocks,
|
|
state.LastBlockTime.Add(evidenceParams.MaxAgeDuration),
|
|
)
|
|
}
|
|
|
|
// apply the evidence-specific verification logic
|
|
switch ev := evidence.(type) {
|
|
case *types.DuplicateVoteEvidence:
|
|
valSet, err := evpool.stateDB.LoadValidators(evidence.Height())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = VerifyDuplicateVote(ev, state.ChainID, valSet)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("verifying duplicate vote evidence: %w", err)
|
|
}
|
|
|
|
_, val := valSet.GetByAddress(ev.VoteA.ValidatorAddress)
|
|
|
|
return &info{
|
|
Evidence: evidence,
|
|
Time: evTime,
|
|
Validators: []*types.Validator{val}, // just a single validator for duplicate vote evidence
|
|
TotalVotingPower: valSet.TotalVotingPower(),
|
|
}, nil
|
|
|
|
case *types.LightClientAttackEvidence:
|
|
commonHeader, err := getSignedHeader(evpool.blockStore, evidence.Height())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
commonVals, err := evpool.stateDB.LoadValidators(evidence.Height())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
trustedHeader := commonHeader
|
|
// in the case of lunatic the trusted header is different to the common header
|
|
if evidence.Height() != ev.ConflictingBlock.Height {
|
|
trustedHeader, err = getSignedHeader(evpool.blockStore, ev.ConflictingBlock.Height)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = VerifyLightClientAttack(ev, commonHeader, trustedHeader, commonVals, state.LastBlockTime,
|
|
state.ConsensusParams.Evidence.MaxAgeDuration)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// find out what type of attack this was and thus extract the malicious validators. Note in the case of an
|
|
// Amnesia attack we don't have any malicious validators.
|
|
validators, attackType := getMaliciousValidators(ev, commonVals, trustedHeader)
|
|
totalVotingPower := ev.ConflictingBlock.ValidatorSet.TotalVotingPower()
|
|
if attackType == lunaticType {
|
|
totalVotingPower = commonVals.TotalVotingPower()
|
|
}
|
|
|
|
return &info{
|
|
Evidence: evidence,
|
|
Time: evTime,
|
|
Validators: validators,
|
|
TotalVotingPower: totalVotingPower,
|
|
}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unrecognized evidence type: %T", evidence)
|
|
}
|
|
}
|
|
|
|
// VerifyLightClientAttack verifies LightClientAttackEvidence against the state of the full node. This involves
|
|
// the following checks:
|
|
// - the common header from the full node has at least 1/3 voting power which is also present in
|
|
// the conflicting header's commit
|
|
// - the nodes trusted header at the same height as the conflicting header has a different hash
|
|
func VerifyLightClientAttack(e *types.LightClientAttackEvidence, commonHeader, trustedHeader *types.SignedHeader,
|
|
commonVals *types.ValidatorSet, now time.Time, trustPeriod time.Duration) error {
|
|
// In the case of lunatic attack we need to perform a single verification jump between the
|
|
// common header and the conflicting one
|
|
if commonHeader.Height != trustedHeader.Height {
|
|
err := light.Verify(commonHeader, commonVals, e.ConflictingBlock.SignedHeader, e.ConflictingBlock.ValidatorSet,
|
|
trustPeriod, now, 0*time.Second, light.DefaultTrustLevel)
|
|
if err != nil {
|
|
return fmt.Errorf("skipping verification from common to conflicting header failed: %w", err)
|
|
}
|
|
} else {
|
|
// in the case of equivocation and amnesia we expect some header hashes to be correctly derived
|
|
if isInvalidHeader(trustedHeader.Header, e.ConflictingBlock.Header) {
|
|
return errors.New("common height is the same as conflicting block height so expected the conflicting" +
|
|
" block to be correctly derived yet it wasn't")
|
|
}
|
|
// ensure that 2/3 of the validator set did vote for this block
|
|
if err := e.ConflictingBlock.ValidatorSet.VerifyCommitLight(trustedHeader.ChainID, e.ConflictingBlock.Commit.BlockID,
|
|
e.ConflictingBlock.Height, e.ConflictingBlock.Commit); err != nil {
|
|
return fmt.Errorf("invalid commit from conflicting block: %w", err)
|
|
}
|
|
}
|
|
|
|
if bytes.Equal(trustedHeader.Hash(), e.ConflictingBlock.Hash()) {
|
|
return fmt.Errorf("trusted header hash matches the evidence conflicting header hash: %X",
|
|
trustedHeader.Hash())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifyDuplicateVote verifies DuplicateVoteEvidence against the state of full node. This involves the
|
|
// following checks:
|
|
// - the validator is in the validator set at the height of the evidence
|
|
// - the height, round, type and validator address of the votes must be the same
|
|
// - the block ID's must be different
|
|
// - The signatures must both be valid
|
|
func VerifyDuplicateVote(e *types.DuplicateVoteEvidence, chainID string, valSet *types.ValidatorSet) error {
|
|
_, val := valSet.GetByAddress(e.VoteA.ValidatorAddress)
|
|
if val == nil {
|
|
return fmt.Errorf("address %X was not a validator at height %d", e.VoteA.ValidatorAddress, e.Height())
|
|
}
|
|
pubKey := val.PubKey
|
|
|
|
// H/R/S must be the same
|
|
if e.VoteA.Height != e.VoteB.Height ||
|
|
e.VoteA.Round != e.VoteB.Round ||
|
|
e.VoteA.Type != e.VoteB.Type {
|
|
return fmt.Errorf("h/r/s does not match: %d/%d/%v vs %d/%d/%v",
|
|
e.VoteA.Height, e.VoteA.Round, e.VoteA.Type,
|
|
e.VoteB.Height, e.VoteB.Round, e.VoteB.Type)
|
|
}
|
|
|
|
// Address must be the same
|
|
if !bytes.Equal(e.VoteA.ValidatorAddress, e.VoteB.ValidatorAddress) {
|
|
return fmt.Errorf("validator addresses do not match: %X vs %X",
|
|
e.VoteA.ValidatorAddress,
|
|
e.VoteB.ValidatorAddress,
|
|
)
|
|
}
|
|
|
|
// BlockIDs must be different
|
|
if e.VoteA.BlockID.Equals(e.VoteB.BlockID) {
|
|
return fmt.Errorf(
|
|
"block IDs are the same (%v) - not a real duplicate vote",
|
|
e.VoteA.BlockID,
|
|
)
|
|
}
|
|
|
|
// pubkey must match address (this should already be true, sanity check)
|
|
addr := e.VoteA.ValidatorAddress
|
|
if !bytes.Equal(pubKey.Address(), addr) {
|
|
return fmt.Errorf("address (%X) doesn't match pubkey (%v - %X)",
|
|
addr, pubKey, pubKey.Address())
|
|
}
|
|
va := e.VoteA.ToProto()
|
|
vb := e.VoteB.ToProto()
|
|
// Signatures must be valid
|
|
if !pubKey.VerifySignature(types.VoteSignBytes(chainID, va), e.VoteA.Signature) {
|
|
return fmt.Errorf("verifying VoteA: %w", types.ErrVoteInvalidSignature)
|
|
}
|
|
if !pubKey.VerifySignature(types.VoteSignBytes(chainID, vb), e.VoteB.Signature) {
|
|
return fmt.Errorf("verifying VoteB: %w", types.ErrVoteInvalidSignature)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getSignedHeader(blockStore BlockStore, height int64) (*types.SignedHeader, error) {
|
|
blockMeta := blockStore.LoadBlockMeta(height)
|
|
if blockMeta == nil {
|
|
return nil, fmt.Errorf("don't have header at height #%d", height)
|
|
}
|
|
commit := blockStore.LoadBlockCommit(height)
|
|
if commit == nil {
|
|
return nil, fmt.Errorf("don't have commit at height #%d", height)
|
|
}
|
|
return &types.SignedHeader{
|
|
Header: &blockMeta.Header,
|
|
Commit: commit,
|
|
}, nil
|
|
}
|
|
|
|
// getMaliciousValidators finds out what style of attack LightClientAttackEvidence was and then works out who
|
|
// the malicious validators were and returns them.
|
|
func getMaliciousValidators(evidence *types.LightClientAttackEvidence, commonVals *types.ValidatorSet,
|
|
trusted *types.SignedHeader) ([]*types.Validator, lightClientAttackType) {
|
|
var validators []*types.Validator
|
|
// First check if the header is invalid. This means that it is a lunatic attack and therefore we take the
|
|
// validators who are in the commonVals and voted for the lunatic header
|
|
if isInvalidHeader(trusted.Header, evidence.ConflictingBlock.Header) {
|
|
for _, commitSig := range evidence.ConflictingBlock.Commit.Signatures {
|
|
if !commitSig.ForBlock() {
|
|
continue
|
|
}
|
|
|
|
_, val := commonVals.GetByAddress(commitSig.ValidatorAddress)
|
|
if val == nil {
|
|
// validator wasn't in the common validator set
|
|
continue
|
|
}
|
|
validators = append(validators, val)
|
|
}
|
|
return validators, lunaticType
|
|
// Next, check to see if it is an equivocation attack and both commits are in the same round. If this is the
|
|
// case then we take the validators from the conflicting light block validator set that voted in both headers.
|
|
} else if trusted.Commit.Round == evidence.ConflictingBlock.Commit.Round {
|
|
// validator hashes are the same therefore the indexing order of validators are the same and thus we
|
|
// only need a single loop to find the validators that voted twice.
|
|
for i := 0; i < len(evidence.ConflictingBlock.Commit.Signatures); i++ {
|
|
sigA := evidence.ConflictingBlock.Commit.Signatures[i]
|
|
if sigA.Absent() {
|
|
continue
|
|
}
|
|
|
|
sigB := trusted.Commit.Signatures[i]
|
|
if sigB.Absent() {
|
|
continue
|
|
}
|
|
|
|
_, val := evidence.ConflictingBlock.ValidatorSet.GetByAddress(sigA.ValidatorAddress)
|
|
validators = append(validators, val)
|
|
}
|
|
return validators, equivocationType
|
|
|
|
}
|
|
// if the rounds are different then this is an amnesia attack. Unfortunately, given the nature of the attack,
|
|
// we aren't able yet to deduce which are malicious validators and which are not hence we return an
|
|
// empty validator set.
|
|
return validators, amnesiaType
|
|
}
|
|
|
|
// isInvalidHeader takes a trusted header and matches it againt a conflicting header
|
|
// to determine whether the conflicting header was the product of a valid state transition
|
|
// or not. If it is then all the deterministic fields of the header should be the same.
|
|
// If not, it is an invalid header and constitutes a lunatic attack.
|
|
func isInvalidHeader(trusted, conflicting *types.Header) bool {
|
|
return !bytes.Equal(trusted.ValidatorsHash, conflicting.ValidatorsHash) ||
|
|
!bytes.Equal(trusted.NextValidatorsHash, conflicting.NextValidatorsHash) ||
|
|
!bytes.Equal(trusted.ConsensusHash, conflicting.ConsensusHash) ||
|
|
!bytes.Equal(trusted.AppHash, conflicting.AppHash) ||
|
|
!bytes.Equal(trusted.LastResultsHash, conflicting.LastResultsHash)
|
|
}
|
|
|
|
type lightClientAttackType int
|
|
|
|
const (
|
|
lunaticType lightClientAttackType = iota + 1
|
|
equivocationType
|
|
amnesiaType
|
|
)
|