evidence: modify evidence types (#5342) light: detect light client attacks (#5344) evidence: refactor evidence pool (#5345) abci: application evidence prepared by evidence pool (#5354)pull/5362/head
@ -0,0 +1,52 @@ | |||
// Code generated by mockery v2.1.0. DO NOT EDIT. | |||
package mocks | |||
import ( | |||
mock "github.com/stretchr/testify/mock" | |||
state "github.com/tendermint/tendermint/state" | |||
types "github.com/tendermint/tendermint/types" | |||
) | |||
// StateStore is an autogenerated mock type for the StateStore type | |||
type StateStore struct { | |||
mock.Mock | |||
} | |||
// LoadState provides a mock function with given fields: | |||
func (_m *StateStore) LoadState() state.State { | |||
ret := _m.Called() | |||
var r0 state.State | |||
if rf, ok := ret.Get(0).(func() state.State); ok { | |||
r0 = rf() | |||
} else { | |||
r0 = ret.Get(0).(state.State) | |||
} | |||
return r0 | |||
} | |||
// LoadValidators provides a mock function with given fields: height | |||
func (_m *StateStore) LoadValidators(height int64) (*types.ValidatorSet, error) { | |||
ret := _m.Called(height) | |||
var r0 *types.ValidatorSet | |||
if rf, ok := ret.Get(0).(func(int64) *types.ValidatorSet); ok { | |||
r0 = rf(height) | |||
} else { | |||
if ret.Get(0) != nil { | |||
r0 = ret.Get(0).(*types.ValidatorSet) | |||
} | |||
} | |||
var r1 error | |||
if rf, ok := ret.Get(1).(func(int64) error); ok { | |||
r1 = rf(height) | |||
} else { | |||
r1 = ret.Error(1) | |||
} | |||
return r0, r1 | |||
} |
@ -1,79 +1,289 @@ | |||
package evidence | |||
import ( | |||
"bytes" | |||
"errors" | |||
"fmt" | |||
"time" | |||
sm "github.com/tendermint/tendermint/state" | |||
"github.com/tendermint/tendermint/light" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// VerifyEvidence verifies the evidence fully by checking: | |||
// 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 | |||
// - it was properly signed by the alleged equivocator | |||
func VerifyEvidence(evidence types.Evidence, state sm.State, stateDB sm.Store, blockStore BlockStore) error { | |||
// - 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 | |||
ageDuration = state.LastBlockTime.Sub(evidence.Time()) | |||
ageNumBlocks = height - evidence.Height() | |||
header *types.Header | |||
ageNumBlocks = height - evidence.Height() | |||
) | |||
// if the evidence is from the current height - this means the evidence is fresh from the consensus | |||
// and we won't have it in the block store. We thus check that the time isn't before the previous block | |||
if evidence.Height() == height+1 { | |||
if evidence.Time().Before(state.LastBlockTime) { | |||
return fmt.Errorf("evidence is from an earlier time than the previous block: %v < %v", | |||
evidence.Time(), | |||
state.LastBlockTime) | |||
} | |||
} else { | |||
// try to retrieve header from blockstore | |||
blockMeta := blockStore.LoadBlockMeta(evidence.Height()) | |||
header = &blockMeta.Header | |||
if header == nil { | |||
return fmt.Errorf("don't have header at height #%d", evidence.Height()) | |||
} | |||
if header.Time != evidence.Time() { | |||
return fmt.Errorf("evidence time (%v) is different to the time of the header we have for the same height (%v)", | |||
evidence.Time(), | |||
header.Time, | |||
) | |||
} | |||
// check that the evidence isn't already committed | |||
if evpool.isCommitted(evidence) { | |||
return nil, errors.New("evidence was already committed") | |||
} | |||
// 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 fmt.Errorf( | |||
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(), | |||
evidence.Time(), | |||
evTime, | |||
height-evidenceParams.MaxAgeNumBlocks, | |||
state.LastBlockTime.Add(evidenceParams.MaxAgeDuration), | |||
) | |||
} | |||
valset, err := stateDB.LoadValidators(evidence.Height()) | |||
if err != nil { | |||
return err | |||
// 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) | |||
} | |||
} | |||
addr := evidence.Address() | |||
var val *types.Validator | |||
// 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) | |||
} | |||
} | |||
// For all other types, expect evidence.Address to be a validator at height | |||
// evidence.Height. | |||
_, val = valset.GetByAddress(addr) | |||
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", addr, evidence.Height()) | |||
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, | |||
) | |||
} | |||
if err := evidence.Verify(state.ChainID, val.PubKey); err != nil { | |||
return err | |||
// 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 | |||
) |
@ -1,79 +1,459 @@ | |||
package evidence | |||
package evidence_test | |||
import ( | |||
"fmt" | |||
"testing" | |||
"time" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/mock" | |||
"github.com/stretchr/testify/require" | |||
dbm "github.com/tendermint/tm-db" | |||
abci "github.com/tendermint/tendermint/abci/types" | |||
"github.com/tendermint/tendermint/crypto" | |||
"github.com/tendermint/tendermint/crypto/tmhash" | |||
"github.com/tendermint/tendermint/evidence" | |||
"github.com/tendermint/tendermint/evidence/mocks" | |||
"github.com/tendermint/tendermint/libs/log" | |||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types" | |||
tmversion "github.com/tendermint/tendermint/proto/tendermint/version" | |||
sm "github.com/tendermint/tendermint/state" | |||
smmocks "github.com/tendermint/tendermint/state/mocks" | |||
"github.com/tendermint/tendermint/types" | |||
"github.com/tendermint/tendermint/version" | |||
) | |||
func TestVerifyEvidenceWrongAddress(t *testing.T) { | |||
var height int64 = 4 | |||
val := types.NewMockPV() | |||
stateStore := initializeValidatorState(val, height) | |||
state, err := stateStore.Load() | |||
if err != nil { | |||
t.Error(err) | |||
func TestVerifyLightClientAttack_Lunatic(t *testing.T) { | |||
commonVals, commonPrivVals := types.RandValidatorSet(2, 10) | |||
newVal, newPrivVal := types.RandValidator(false, 9) | |||
conflictingVals, err := types.ValidatorSetFromExistingValidators(append(commonVals.Validators, newVal)) | |||
require.NoError(t, err) | |||
conflictingPrivVals := append(commonPrivVals, newPrivVal) | |||
commonHeader := makeHeaderRandom(4) | |||
commonHeader.Time = defaultEvidenceTime.Add(-1 * time.Hour) | |||
trustedHeader := makeHeaderRandom(10) | |||
conflictingHeader := makeHeaderRandom(10) | |||
conflictingHeader.ValidatorsHash = conflictingVals.Hash() | |||
// we are simulating a duplicate vote attack where all the validators in the conflictingVals set | |||
// vote twice | |||
blockID := makeBlockID(conflictingHeader.Hash(), 1000, []byte("partshash")) | |||
voteSet := types.NewVoteSet(evidenceChainID, 10, 1, tmproto.SignedMsgType(2), conflictingVals) | |||
commit, err := types.MakeCommit(blockID, 10, 1, voteSet, conflictingPrivVals, defaultEvidenceTime) | |||
require.NoError(t, err) | |||
ev := &types.LightClientAttackEvidence{ | |||
ConflictingBlock: &types.LightBlock{ | |||
SignedHeader: &types.SignedHeader{ | |||
Header: conflictingHeader, | |||
Commit: commit, | |||
}, | |||
ValidatorSet: conflictingVals, | |||
}, | |||
CommonHeight: 4, | |||
} | |||
commonSignedHeader := &types.SignedHeader{ | |||
Header: commonHeader, | |||
Commit: &types.Commit{}, | |||
} | |||
trustedBlockID := makeBlockID(trustedHeader.Hash(), 1000, []byte("partshash")) | |||
vals, privVals := types.RandValidatorSet(3, 8) | |||
trustedVoteSet := types.NewVoteSet(evidenceChainID, 10, 1, tmproto.SignedMsgType(2), vals) | |||
trustedCommit, err := types.MakeCommit(trustedBlockID, 10, 1, trustedVoteSet, privVals, defaultEvidenceTime) | |||
require.NoError(t, err) | |||
trustedSignedHeader := &types.SignedHeader{ | |||
Header: trustedHeader, | |||
Commit: trustedCommit, | |||
} | |||
// good pass -> no error | |||
err = evidence.VerifyLightClientAttack(ev, commonSignedHeader, trustedSignedHeader, commonVals, | |||
defaultEvidenceTime.Add(1*time.Minute), 2*time.Hour) | |||
assert.NoError(t, err) | |||
// trusted and conflicting hashes are the same -> an error should be returned | |||
err = evidence.VerifyLightClientAttack(ev, commonSignedHeader, ev.ConflictingBlock.SignedHeader, commonVals, | |||
defaultEvidenceTime.Add(1*time.Minute), 2*time.Hour) | |||
assert.Error(t, err) | |||
state := sm.State{ | |||
LastBlockTime: defaultEvidenceTime.Add(1 * time.Minute), | |||
LastBlockHeight: 11, | |||
ConsensusParams: *types.DefaultConsensusParams(), | |||
} | |||
stateStore := &smmocks.Store{} | |||
stateStore.On("LoadValidators", int64(4)).Return(commonVals, nil) | |||
stateStore.On("Load").Return(state, nil) | |||
blockStore := &mocks.BlockStore{} | |||
blockStore.On("LoadBlockMeta", mock.AnythingOfType("int64")).Return( | |||
&types.BlockMeta{Header: types.Header{Time: defaultEvidenceTime}}, | |||
) | |||
evidence := types.NewMockDuplicateVoteEvidence(1, defaultEvidenceTime, evidenceChainID) | |||
err = VerifyEvidence(evidence, state, stateStore, blockStore) | |||
errMsg := fmt.Sprintf("address %X was not a validator at height 1", evidence.Address()) | |||
if assert.Error(t, err) { | |||
assert.Equal(t, err.Error(), errMsg) | |||
blockStore.On("LoadBlockMeta", int64(4)).Return(&types.BlockMeta{Header: *commonHeader}) | |||
blockStore.On("LoadBlockMeta", int64(10)).Return(&types.BlockMeta{Header: *trustedHeader}) | |||
blockStore.On("LoadBlockCommit", int64(4)).Return(commit) | |||
blockStore.On("LoadBlockCommit", int64(10)).Return(trustedCommit) | |||
pool, err := evidence.NewPool(dbm.NewMemDB(), stateStore, blockStore) | |||
require.NoError(t, err) | |||
pool.SetLogger(log.TestingLogger()) | |||
evList := types.EvidenceList{ev} | |||
err = pool.CheckEvidence(evList) | |||
assert.NoError(t, err) | |||
pendingEvs := pool.PendingEvidence(2) | |||
assert.Equal(t, 1, len(pendingEvs)) | |||
pubKey, err := newPrivVal.GetPubKey() | |||
require.NoError(t, err) | |||
lastCommit := makeCommit(state.LastBlockHeight, pubKey.Address()) | |||
block := types.MakeBlock(state.LastBlockHeight, []types.Tx{}, lastCommit, []types.Evidence{ev}) | |||
abciEv := pool.ABCIEvidence(block.Height, block.Evidence.Evidence) | |||
expectedAbciEv := make([]abci.Evidence, len(commonVals.Validators)) | |||
// we expect evidence to be made for all validators in the common validator set | |||
for idx, val := range commonVals.Validators { | |||
ev := abci.Evidence{ | |||
Type: abci.EvidenceType_LIGHT_CLIENT_ATTACK, | |||
Validator: types.TM2PB.Validator(val), | |||
Height: commonHeader.Height, | |||
Time: commonHeader.Time, | |||
TotalVotingPower: commonVals.TotalVotingPower(), | |||
} | |||
expectedAbciEv[idx] = ev | |||
} | |||
assert.Equal(t, expectedAbciEv, abciEv) | |||
} | |||
func TestVerifyEvidenceExpiredEvidence(t *testing.T) { | |||
var height int64 = 4 | |||
val := types.NewMockPV() | |||
stateStore := initializeValidatorState(val, height) | |||
state, err := stateStore.Load() | |||
if err != nil { | |||
t.Error(err) | |||
func TestVerifyLightClientAttack_Equivocation(t *testing.T) { | |||
conflictingVals, conflictingPrivVals := types.RandValidatorSet(5, 10) | |||
trustedHeader := makeHeaderRandom(10) | |||
conflictingHeader := makeHeaderRandom(10) | |||
conflictingHeader.ValidatorsHash = conflictingVals.Hash() | |||
trustedHeader.ValidatorsHash = conflictingHeader.ValidatorsHash | |||
trustedHeader.NextValidatorsHash = conflictingHeader.NextValidatorsHash | |||
trustedHeader.ConsensusHash = conflictingHeader.ConsensusHash | |||
trustedHeader.AppHash = conflictingHeader.AppHash | |||
trustedHeader.LastResultsHash = conflictingHeader.LastResultsHash | |||
// we are simulating a duplicate vote attack where all the validators in the conflictingVals set | |||
// except the last validator vote twice | |||
blockID := makeBlockID(conflictingHeader.Hash(), 1000, []byte("partshash")) | |||
voteSet := types.NewVoteSet(evidenceChainID, 10, 1, tmproto.SignedMsgType(2), conflictingVals) | |||
commit, err := types.MakeCommit(blockID, 10, 1, voteSet, conflictingPrivVals[:4], defaultEvidenceTime) | |||
require.NoError(t, err) | |||
ev := &types.LightClientAttackEvidence{ | |||
ConflictingBlock: &types.LightBlock{ | |||
SignedHeader: &types.SignedHeader{ | |||
Header: conflictingHeader, | |||
Commit: commit, | |||
}, | |||
ValidatorSet: conflictingVals, | |||
}, | |||
CommonHeight: 10, | |||
} | |||
state.ConsensusParams.Evidence.MaxAgeNumBlocks = 1 | |||
expiredEvidenceTime := time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC) | |||
trustedBlockID := makeBlockID(trustedHeader.Hash(), 1000, []byte("partshash")) | |||
trustedVoteSet := types.NewVoteSet(evidenceChainID, 10, 1, tmproto.SignedMsgType(2), conflictingVals) | |||
trustedCommit, err := types.MakeCommit(trustedBlockID, 10, 1, trustedVoteSet, conflictingPrivVals, defaultEvidenceTime) | |||
require.NoError(t, err) | |||
trustedSignedHeader := &types.SignedHeader{ | |||
Header: trustedHeader, | |||
Commit: trustedCommit, | |||
} | |||
// good pass -> no error | |||
err = evidence.VerifyLightClientAttack(ev, trustedSignedHeader, trustedSignedHeader, nil, | |||
defaultEvidenceTime.Add(1*time.Minute), 2*time.Hour) | |||
assert.NoError(t, err) | |||
// trusted and conflicting hashes are the same -> an error should be returned | |||
err = evidence.VerifyLightClientAttack(ev, trustedSignedHeader, ev.ConflictingBlock.SignedHeader, nil, | |||
defaultEvidenceTime.Add(1*time.Minute), 2*time.Hour) | |||
assert.Error(t, err) | |||
// conflicting header has different next validators hash which should have been correctly derived from | |||
// the previous round | |||
ev.ConflictingBlock.Header.NextValidatorsHash = crypto.CRandBytes(tmhash.Size) | |||
err = evidence.VerifyLightClientAttack(ev, trustedSignedHeader, trustedSignedHeader, nil, | |||
defaultEvidenceTime.Add(1*time.Minute), 2*time.Hour) | |||
assert.Error(t, err) | |||
// revert next validators hash | |||
ev.ConflictingBlock.Header.NextValidatorsHash = trustedHeader.NextValidatorsHash | |||
state := sm.State{ | |||
LastBlockTime: defaultEvidenceTime.Add(1 * time.Minute), | |||
LastBlockHeight: 11, | |||
ConsensusParams: *types.DefaultConsensusParams(), | |||
} | |||
stateStore := &smmocks.Store{} | |||
stateStore.On("LoadValidators", int64(10)).Return(conflictingVals, nil) | |||
stateStore.On("Load").Return(state, nil) | |||
blockStore := &mocks.BlockStore{} | |||
blockStore.On("LoadBlockMeta", mock.AnythingOfType("int64")).Return( | |||
&types.BlockMeta{Header: types.Header{Time: expiredEvidenceTime}}, | |||
) | |||
blockStore.On("LoadBlockMeta", int64(10)).Return(&types.BlockMeta{Header: *trustedHeader}) | |||
blockStore.On("LoadBlockCommit", int64(10)).Return(trustedCommit) | |||
pool, err := evidence.NewPool(dbm.NewMemDB(), stateStore, blockStore) | |||
require.NoError(t, err) | |||
pool.SetLogger(log.TestingLogger()) | |||
evList := types.EvidenceList{ev} | |||
err = pool.CheckEvidence(evList) | |||
assert.NoError(t, err) | |||
pendingEvs := pool.PendingEvidence(2) | |||
assert.Equal(t, 1, len(pendingEvs)) | |||
pubKey, err := conflictingPrivVals[0].GetPubKey() | |||
require.NoError(t, err) | |||
lastCommit := makeCommit(state.LastBlockHeight, pubKey.Address()) | |||
block := types.MakeBlock(state.LastBlockHeight, []types.Tx{}, lastCommit, []types.Evidence{ev}) | |||
abciEv := pool.ABCIEvidence(block.Height, block.Evidence.Evidence) | |||
expectedAbciEv := make([]abci.Evidence, len(conflictingVals.Validators)-1) | |||
// we expect evidence to be made for all validators except the last one | |||
for idx, val := range conflictingVals.Validators { | |||
if idx == 4 { // skip the last validator | |||
continue | |||
} | |||
ev := abci.Evidence{ | |||
Type: abci.EvidenceType_LIGHT_CLIENT_ATTACK, | |||
Validator: types.TM2PB.Validator(val), | |||
Height: ev.ConflictingBlock.Height, | |||
Time: ev.ConflictingBlock.Time, | |||
TotalVotingPower: ev.ConflictingBlock.ValidatorSet.TotalVotingPower(), | |||
} | |||
expectedAbciEv[idx] = ev | |||
} | |||
expiredEv := types.NewMockDuplicateVoteEvidenceWithValidator(1, expiredEvidenceTime, val, evidenceChainID) | |||
err = VerifyEvidence(expiredEv, state, stateStore, blockStore) | |||
errMsg := "evidence from height 1 (created at: 2018-01-01 00:00:00 +0000 UTC) is too old" | |||
if assert.Error(t, err) { | |||
assert.Equal(t, err.Error()[:len(errMsg)], errMsg) | |||
assert.Equal(t, expectedAbciEv, abciEv) | |||
} | |||
func TestVerifyLightClientAttack_Amnesia(t *testing.T) { | |||
conflictingVals, conflictingPrivVals := types.RandValidatorSet(5, 10) | |||
conflictingHeader := makeHeaderRandom(10) | |||
conflictingHeader.ValidatorsHash = conflictingVals.Hash() | |||
trustedHeader := makeHeaderRandom(10) | |||
trustedHeader.ValidatorsHash = conflictingHeader.ValidatorsHash | |||
trustedHeader.NextValidatorsHash = conflictingHeader.NextValidatorsHash | |||
trustedHeader.AppHash = conflictingHeader.AppHash | |||
trustedHeader.ConsensusHash = conflictingHeader.ConsensusHash | |||
trustedHeader.LastResultsHash = conflictingHeader.LastResultsHash | |||
// we are simulating an amnesia attack where all the validators in the conflictingVals set | |||
// except the last validator vote twice. However this time the commits are of different rounds. | |||
blockID := makeBlockID(conflictingHeader.Hash(), 1000, []byte("partshash")) | |||
voteSet := types.NewVoteSet(evidenceChainID, 10, 0, tmproto.SignedMsgType(2), conflictingVals) | |||
commit, err := types.MakeCommit(blockID, 10, 0, voteSet, conflictingPrivVals, defaultEvidenceTime) | |||
require.NoError(t, err) | |||
ev := &types.LightClientAttackEvidence{ | |||
ConflictingBlock: &types.LightBlock{ | |||
SignedHeader: &types.SignedHeader{ | |||
Header: conflictingHeader, | |||
Commit: commit, | |||
}, | |||
ValidatorSet: conflictingVals, | |||
}, | |||
CommonHeight: 10, | |||
} | |||
trustedBlockID := makeBlockID(trustedHeader.Hash(), 1000, []byte("partshash")) | |||
trustedVoteSet := types.NewVoteSet(evidenceChainID, 10, 1, tmproto.SignedMsgType(2), conflictingVals) | |||
trustedCommit, err := types.MakeCommit(trustedBlockID, 10, 1, trustedVoteSet, conflictingPrivVals, defaultEvidenceTime) | |||
require.NoError(t, err) | |||
trustedSignedHeader := &types.SignedHeader{ | |||
Header: trustedHeader, | |||
Commit: trustedCommit, | |||
} | |||
// good pass -> no error | |||
err = evidence.VerifyLightClientAttack(ev, trustedSignedHeader, trustedSignedHeader, nil, | |||
defaultEvidenceTime.Add(1*time.Minute), 2*time.Hour) | |||
assert.NoError(t, err) | |||
// trusted and conflicting hashes are the same -> an error should be returned | |||
err = evidence.VerifyLightClientAttack(ev, trustedSignedHeader, ev.ConflictingBlock.SignedHeader, nil, | |||
defaultEvidenceTime.Add(1*time.Minute), 2*time.Hour) | |||
assert.Error(t, err) | |||
state := sm.State{ | |||
LastBlockTime: defaultEvidenceTime.Add(1 * time.Minute), | |||
LastBlockHeight: 11, | |||
ConsensusParams: *types.DefaultConsensusParams(), | |||
} | |||
stateStore := &smmocks.Store{} | |||
stateStore.On("LoadValidators", int64(10)).Return(conflictingVals, nil) | |||
stateStore.On("Load").Return(state, nil) | |||
blockStore := &mocks.BlockStore{} | |||
blockStore.On("LoadBlockMeta", int64(10)).Return(&types.BlockMeta{Header: *trustedHeader}) | |||
blockStore.On("LoadBlockCommit", int64(10)).Return(trustedCommit) | |||
pool, err := evidence.NewPool(dbm.NewMemDB(), stateStore, blockStore) | |||
require.NoError(t, err) | |||
pool.SetLogger(log.TestingLogger()) | |||
evList := types.EvidenceList{ev} | |||
err = pool.CheckEvidence(evList) | |||
assert.NoError(t, err) | |||
pendingEvs := pool.PendingEvidence(2) | |||
assert.Equal(t, 1, len(pendingEvs)) | |||
pubKey, err := conflictingPrivVals[0].GetPubKey() | |||
require.NoError(t, err) | |||
lastCommit := makeCommit(state.LastBlockHeight, pubKey.Address()) | |||
block := types.MakeBlock(state.LastBlockHeight, []types.Tx{}, lastCommit, []types.Evidence{ev}) | |||
abciEv := pool.ABCIEvidence(block.Height, block.Evidence.Evidence) | |||
// as we are unable to find out which subset of validators in the commit were malicious, no information | |||
// is sent to the application. We expect the array to be empty | |||
emptyEvidenceBlock := types.MakeBlock(state.LastBlockHeight, []types.Tx{}, lastCommit, []types.Evidence{}) | |||
expectedAbciEv := pool.ABCIEvidence(emptyEvidenceBlock.Height, emptyEvidenceBlock.Evidence.Evidence) | |||
assert.Equal(t, expectedAbciEv, abciEv) | |||
} | |||
func TestVerifyEvidenceInvalidTime(t *testing.T) { | |||
height := int64(4) | |||
type voteData struct { | |||
vote1 *types.Vote | |||
vote2 *types.Vote | |||
valid bool | |||
} | |||
func TestVerifyDuplicateVoteEvidence(t *testing.T) { | |||
val := types.NewMockPV() | |||
stateStore := initializeValidatorState(val, height) | |||
state, err := stateStore.Load() | |||
if err != nil { | |||
t.Error(err) | |||
val2 := types.NewMockPV() | |||
valSet := types.NewValidatorSet([]*types.Validator{val.ExtractIntoValidator(1)}) | |||
blockID := makeBlockID([]byte("blockhash"), 1000, []byte("partshash")) | |||
blockID2 := makeBlockID([]byte("blockhash2"), 1000, []byte("partshash")) | |||
blockID3 := makeBlockID([]byte("blockhash"), 10000, []byte("partshash")) | |||
blockID4 := makeBlockID([]byte("blockhash"), 10000, []byte("partshash2")) | |||
const chainID = "mychain" | |||
vote1 := makeVote(t, val, chainID, 0, 10, 2, 1, blockID, defaultEvidenceTime) | |||
v1 := vote1.ToProto() | |||
err := val.SignVote(chainID, v1) | |||
require.NoError(t, err) | |||
badVote := makeVote(t, val, chainID, 0, 10, 2, 1, blockID, defaultEvidenceTime) | |||
bv := badVote.ToProto() | |||
err = val2.SignVote(chainID, bv) | |||
require.NoError(t, err) | |||
vote1.Signature = v1.Signature | |||
badVote.Signature = bv.Signature | |||
cases := []voteData{ | |||
{vote1, makeVote(t, val, chainID, 0, 10, 2, 1, blockID2, defaultEvidenceTime), true}, // different block ids | |||
{vote1, makeVote(t, val, chainID, 0, 10, 2, 1, blockID3, defaultEvidenceTime), true}, | |||
{vote1, makeVote(t, val, chainID, 0, 10, 2, 1, blockID4, defaultEvidenceTime), true}, | |||
{vote1, makeVote(t, val, chainID, 0, 10, 2, 1, blockID, defaultEvidenceTime), false}, // wrong block id | |||
{vote1, makeVote(t, val, "mychain2", 0, 10, 2, 1, blockID2, defaultEvidenceTime), false}, // wrong chain id | |||
{vote1, makeVote(t, val, chainID, 0, 11, 2, 1, blockID2, defaultEvidenceTime), false}, // wrong height | |||
{vote1, makeVote(t, val, chainID, 0, 10, 3, 1, blockID2, defaultEvidenceTime), false}, // wrong round | |||
{vote1, makeVote(t, val, chainID, 0, 10, 2, 2, blockID2, defaultEvidenceTime), false}, // wrong step | |||
{vote1, makeVote(t, val2, chainID, 0, 10, 2, 1, blockID2, defaultEvidenceTime), false}, // wrong validator | |||
// a different vote time doesn't matter | |||
{vote1, makeVote(t, val, chainID, 0, 10, 2, 1, blockID2, time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)), true}, | |||
{vote1, badVote, false}, // signed by wrong key | |||
} | |||
require.NoError(t, err) | |||
for _, c := range cases { | |||
ev := &types.DuplicateVoteEvidence{ | |||
VoteA: c.vote1, | |||
VoteB: c.vote2, | |||
} | |||
if c.valid { | |||
assert.Nil(t, evidence.VerifyDuplicateVote(ev, chainID, valSet), "evidence should be valid") | |||
} else { | |||
assert.NotNil(t, evidence.VerifyDuplicateVote(ev, chainID, valSet), "evidence should be invalid") | |||
} | |||
} | |||
goodEv := types.NewMockDuplicateVoteEvidenceWithValidator(10, defaultEvidenceTime, val, chainID) | |||
state := sm.State{ | |||
ChainID: chainID, | |||
LastBlockTime: defaultEvidenceTime.Add(1 * time.Minute), | |||
LastBlockHeight: 11, | |||
ConsensusParams: *types.DefaultConsensusParams(), | |||
} | |||
stateStore := &smmocks.Store{} | |||
stateStore.On("LoadValidators", int64(10)).Return(valSet, nil) | |||
stateStore.On("Load").Return(state, nil) | |||
blockStore := &mocks.BlockStore{} | |||
blockStore.On("LoadBlockMeta", mock.AnythingOfType("int64")).Return( | |||
&types.BlockMeta{Header: types.Header{Time: defaultEvidenceTime}}, | |||
) | |||
blockStore.On("LoadBlockMeta", int64(10)).Return(&types.BlockMeta{Header: types.Header{Time: defaultEvidenceTime}}) | |||
pool, err := evidence.NewPool(dbm.NewMemDB(), stateStore, blockStore) | |||
require.NoError(t, err) | |||
evList := types.EvidenceList{goodEv} | |||
err = pool.CheckEvidence(evList) | |||
assert.NoError(t, err) | |||
} | |||
differentTime := time.Date(2019, 2, 1, 0, 0, 0, 0, time.UTC) | |||
ev := types.NewMockDuplicateVoteEvidenceWithValidator(height, differentTime, val, evidenceChainID) | |||
err = VerifyEvidence(ev, state, stateStore, blockStore) | |||
errMsg := "evidence time (2019-02-01 00:00:00 +0000 UTC) is different to the time" + | |||
" of the header we have for the same height (2019-01-01 00:00:00 +0000 UTC)" | |||
if assert.Error(t, err) { | |||
assert.Equal(t, errMsg, err.Error()) | |||
func makeVote( | |||
t *testing.T, val types.PrivValidator, chainID string, valIndex int32, height int64, | |||
round int32, step int, blockID types.BlockID, time time.Time) *types.Vote { | |||
pubKey, err := val.GetPubKey() | |||
require.NoError(t, err) | |||
v := &types.Vote{ | |||
ValidatorAddress: pubKey.Address(), | |||
ValidatorIndex: valIndex, | |||
Height: height, | |||
Round: round, | |||
Type: tmproto.SignedMsgType(step), | |||
BlockID: blockID, | |||
Timestamp: time, | |||
} | |||
vpb := v.ToProto() | |||
err = val.SignVote(chainID, vpb) | |||
if err != nil { | |||
panic(err) | |||
} | |||
v.Signature = vpb.Signature | |||
return v | |||
} | |||
func makeHeaderRandom(height int64) *types.Header { | |||
return &types.Header{ | |||
Version: tmversion.Consensus{Block: version.BlockProtocol, App: 1}, | |||
ChainID: evidenceChainID, | |||
Height: height, | |||
Time: defaultEvidenceTime, | |||
LastBlockID: makeBlockID([]byte("headerhash"), 1000, []byte("partshash")), | |||
LastCommitHash: crypto.CRandBytes(tmhash.Size), | |||
DataHash: crypto.CRandBytes(tmhash.Size), | |||
ValidatorsHash: crypto.CRandBytes(tmhash.Size), | |||
NextValidatorsHash: crypto.CRandBytes(tmhash.Size), | |||
ConsensusHash: crypto.CRandBytes(tmhash.Size), | |||
AppHash: crypto.CRandBytes(tmhash.Size), | |||
LastResultsHash: crypto.CRandBytes(tmhash.Size), | |||
EvidenceHash: crypto.CRandBytes(tmhash.Size), | |||
ProposerAddress: crypto.CRandBytes(crypto.AddressSize), | |||
} | |||
} | |||
func makeBlockID(hash []byte, partSetSize uint32, partSetHash []byte) types.BlockID { | |||
var ( | |||
h = make([]byte, tmhash.Size) | |||
psH = make([]byte, tmhash.Size) | |||
) | |||
copy(h, hash) | |||
copy(psH, partSetHash) | |||
return types.BlockID{ | |||
Hash: h, | |||
PartSetHeader: types.PartSetHeader{ | |||
Total: partSetSize, | |||
Hash: psH, | |||
}, | |||
} | |||
} |
@ -0,0 +1,247 @@ | |||
package light | |||
import ( | |||
"bytes" | |||
"errors" | |||
"fmt" | |||
"time" | |||
"github.com/tendermint/tendermint/light/provider" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// The detector component of the light client detect and handles attacks on the light client. | |||
// More info here: | |||
// tendermint/docs/architecture/adr-047-handling-evidence-from-light-client.md | |||
// detectDivergence is a second wall of defense for the light client and is used | |||
// only in the case of skipping verification which employs the trust level mechanism. | |||
// | |||
// It takes the target verified header and compares it with the headers of a set of | |||
// witness providers that the light client is connected to. If a conflicting header | |||
// is returned it verifies and examines the conflicting header against the verified | |||
// trace that was produced from the primary. If successful it produces two sets of evidence | |||
// and sends them to the opposite provider before halting. | |||
// | |||
// If there are no conflictinge headers, the light client deems the verified target header | |||
// trusted and saves it to the trusted store. | |||
func (c *Client) detectDivergence(primaryTrace []*types.LightBlock, now time.Time) error { | |||
if primaryTrace == nil || len(primaryTrace) < 2 { | |||
return errors.New("nil or single block primary trace") | |||
} | |||
var ( | |||
headerMatched bool | |||
lastVerifiedHeader = primaryTrace[len(primaryTrace)-1].SignedHeader | |||
witnessesToRemove = make([]int, 0) | |||
) | |||
c.logger.Info("Running detector against trace", "endBlockHeight", lastVerifiedHeader.Height, | |||
"endBlockHash", lastVerifiedHeader.Hash, "length", len(primaryTrace)) | |||
c.providerMutex.Lock() | |||
defer c.providerMutex.Unlock() | |||
if len(c.witnesses) == 0 { | |||
return errNoWitnesses{} | |||
} | |||
// launch one goroutine per witness to retrieve the light block of the target height | |||
// and compare it with the header from the primary | |||
errc := make(chan error, len(c.witnesses)) | |||
for i, witness := range c.witnesses { | |||
go c.compareNewHeaderWithWitness(errc, lastVerifiedHeader, witness, i) | |||
} | |||
// handle errors from the header comparisons as they come in | |||
for i := 0; i < cap(errc); i++ { | |||
err := <-errc | |||
switch e := err.(type) { | |||
case nil: // at least one header matched | |||
headerMatched = true | |||
case errConflictingHeaders: | |||
// We have conflicting headers. This could possibly imply an attack on the light client. | |||
// First we need to verify the witness's header using the same skipping verification and then we | |||
// need to find the point that the headers diverge and examine this for any evidence of an attack. | |||
// | |||
// We combine these actions together, verifying the witnesses headers and outputting the trace | |||
// which captures the bifurcation point and if successful provides the information to create | |||
supportingWitness := c.witnesses[e.WitnessIndex] | |||
witnessTrace, primaryBlock, err := c.examineConflictingHeaderAgainstTrace(primaryTrace, e.Block.SignedHeader, | |||
supportingWitness, now) | |||
if err != nil { | |||
c.logger.Info("Error validating witness's divergent header", "witness", supportingWitness, "err", err) | |||
witnessesToRemove = append(witnessesToRemove, e.WitnessIndex) | |||
continue | |||
} | |||
// if this is an equivocation or amnesia attack, i.e. the validator sets are the same, then we | |||
// return the height of the conflicting block else if it is a lunatic attack and the validator sets | |||
// are not the same then we send the height of the common header. | |||
commonHeight := primaryBlock.Height | |||
if isInvalidHeader(witnessTrace[len(witnessTrace)-1].Header, primaryBlock.Header) { | |||
// height of the common header | |||
commonHeight = witnessTrace[0].Height | |||
} | |||
// We are suspecting that the primary is faulty, hence we hold the witness as the source of truth | |||
// and generate evidence against the primary that we can send to the witness | |||
ev := &types.LightClientAttackEvidence{ | |||
ConflictingBlock: primaryBlock, | |||
CommonHeight: commonHeight, // the first block in the bisection is common to both providers | |||
} | |||
c.logger.Error("Attack detected. Sending evidence againt primary by witness", "ev", ev, | |||
"primary", c.primary, "witness", supportingWitness) | |||
c.sendEvidence(ev, supportingWitness) | |||
// This may not be valid because the witness itself is at fault. So now we reverse it, examining the | |||
// trace provided by the witness and holding the primary as the source of truth. Note: primary may not | |||
// respond but this is okay as we will halt anyway. | |||
primaryTrace, witnessBlock, err := c.examineConflictingHeaderAgainstTrace(witnessTrace, primaryBlock.SignedHeader, | |||
c.primary, now) | |||
if err != nil { | |||
c.logger.Info("Error validating primary's divergent header", "primary", c.primary, "err", err) | |||
continue | |||
} | |||
// if this is an equivocation or amnesia attack, i.e. the validator sets are the same, then we | |||
// return the height of the conflicting block else if it is a lunatic attack and the validator sets | |||
// are not the same then we send the height of the common header. | |||
commonHeight = primaryBlock.Height | |||
if isInvalidHeader(primaryTrace[len(primaryTrace)-1].Header, witnessBlock.Header) { | |||
// height of the common header | |||
commonHeight = primaryTrace[0].Height | |||
} | |||
// We now use the primary trace to create evidence against the witness and send it to the primary | |||
ev = &types.LightClientAttackEvidence{ | |||
ConflictingBlock: witnessBlock, | |||
CommonHeight: commonHeight, // the first block in the bisection is common to both providers | |||
} | |||
c.logger.Error("Sending evidence against witness by primary", "ev", ev, | |||
"primary", c.primary, "witness", supportingWitness) | |||
c.sendEvidence(ev, c.primary) | |||
// We return the error and don't process anymore witnesses | |||
return e | |||
case errBadWitness: | |||
c.logger.Info("Witness returned an error during header comparison", "witness", c.witnesses[e.WitnessIndex], | |||
"err", err) | |||
// if witness sent us an invalid header, then remove it. If it didn't respond or couldn't find the block, then we | |||
// ignore it and move on to the next witness | |||
if _, ok := e.Reason.(provider.ErrBadLightBlock); ok { | |||
c.logger.Info("Witness sent us invalid header / vals -> removing it", "witness", c.witnesses[e.WitnessIndex]) | |||
witnessesToRemove = append(witnessesToRemove, e.WitnessIndex) | |||
} | |||
} | |||
} | |||
for _, idx := range witnessesToRemove { | |||
c.removeWitness(idx) | |||
} | |||
// 1. If we had at least one witness that returned the same header then we | |||
// conclude that we can trust the header | |||
if headerMatched { | |||
return nil | |||
} | |||
// 2. ELse all witnesses have either not responded, don't have the block or sent invalid blocks. | |||
return ErrFailedHeaderCrossReferencing | |||
} | |||
// compareNewHeaderWithWitness takes the verified header from the primary and compares it with a | |||
// header from a specified witness. The function can return one of three errors: | |||
// | |||
// 1: errConflictingHeaders -> there may have been an attack on this light client | |||
// 2: errBadWitness -> the witness has either not responded, doesn't have the header or has given us an invalid one | |||
// Note: In the case of an invalid header we remove the witness | |||
// 3: nil -> the hashes of the two headers match | |||
func (c *Client) compareNewHeaderWithWitness(errc chan error, h *types.SignedHeader, | |||
witness provider.Provider, witnessIndex int) { | |||
lightBlock, err := witness.LightBlock(h.Height) | |||
if err != nil { | |||
errc <- errBadWitness{Reason: err, WitnessIndex: witnessIndex} | |||
return | |||
} | |||
if !bytes.Equal(h.Hash(), lightBlock.Hash()) { | |||
errc <- errConflictingHeaders{Block: lightBlock, WitnessIndex: witnessIndex} | |||
} | |||
c.logger.Info("Matching header received by witness", "height", h.Height, "witness", witnessIndex) | |||
errc <- nil | |||
} | |||
// sendEvidence sends evidence to a provider on a best effort basis. | |||
func (c *Client) sendEvidence(ev *types.LightClientAttackEvidence, receiver provider.Provider) { | |||
err := receiver.ReportEvidence(ev) | |||
if err != nil { | |||
c.logger.Error("Failed to report evidence to provider", "ev", ev, "provider", receiver) | |||
} | |||
} | |||
// examineConflictingHeaderAgainstTrace takes a trace from one provider and a divergent header that | |||
// it has received from another and preforms verifySkipping at the heights of each of the intermediate | |||
// headers in the trace until it reaches the divergentHeader. 1 of 2 things can happen. | |||
// | |||
// 1. The light client verifies a header that is different to the intermediate header in the trace. This | |||
// is the bifurcation point and the light client can create evidence from it | |||
// 2. The source stops responding, doesn't have the block or sends an invalid header in which case we | |||
// return the error and remove the witness | |||
func (c *Client) examineConflictingHeaderAgainstTrace( | |||
trace []*types.LightBlock, | |||
divergentHeader *types.SignedHeader, | |||
source provider.Provider, now time.Time) ([]*types.LightBlock, *types.LightBlock, error) { | |||
var previouslyVerifiedBlock *types.LightBlock | |||
for idx, traceBlock := range trace { | |||
// The first block in the trace MUST be the same to the light block that the source produces | |||
// else we cannot continue with verification. | |||
sourceBlock, err := source.LightBlock(traceBlock.Height) | |||
if err != nil { | |||
return nil, nil, err | |||
} | |||
if idx == 0 { | |||
if shash, thash := sourceBlock.Hash(), traceBlock.Hash(); !bytes.Equal(shash, thash) { | |||
return nil, nil, fmt.Errorf("trusted block is different to the source's first block (%X = %X)", | |||
thash, shash) | |||
} | |||
previouslyVerifiedBlock = sourceBlock | |||
continue | |||
} | |||
// we check that the source provider can verify a block at the same height of the | |||
// intermediate height | |||
trace, err := c.verifySkipping(source, previouslyVerifiedBlock, sourceBlock, now) | |||
if err != nil { | |||
return nil, nil, fmt.Errorf("verifySkipping of conflicting header failed: %w", err) | |||
} | |||
// check if the headers verified by the source has diverged from the trace | |||
if shash, thash := sourceBlock.Hash(), traceBlock.Hash(); !bytes.Equal(shash, thash) { | |||
// Bifurcation point found! | |||
return trace, traceBlock, nil | |||
} | |||
// headers are still the same. update the previouslyVerifiedBlock | |||
previouslyVerifiedBlock = sourceBlock | |||
} | |||
// We have reached the end of the trace without observing a divergence. The last header is thus different | |||
// from the divergent header that the source originally sent us, then we return an error. | |||
return nil, nil, fmt.Errorf("source provided different header to the original header it provided (%X != %X)", | |||
previouslyVerifiedBlock.Hash(), divergentHeader.Hash()) | |||
} | |||
// 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) | |||
} |
@ -0,0 +1,210 @@ | |||
package light_test | |||
import ( | |||
"testing" | |||
"time" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
dbm "github.com/tendermint/tm-db" | |||
"github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/light" | |||
"github.com/tendermint/tendermint/light/provider" | |||
mockp "github.com/tendermint/tendermint/light/provider/mock" | |||
dbs "github.com/tendermint/tendermint/light/store/db" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
func TestLightClientAttackEvidence_Lunatic(t *testing.T) { | |||
// primary performs a lunatic attack | |||
var ( | |||
latestHeight = int64(10) | |||
valSize = 5 | |||
divergenceHeight = int64(6) | |||
primaryHeaders = make(map[int64]*types.SignedHeader, latestHeight) | |||
primaryValidators = make(map[int64]*types.ValidatorSet, latestHeight) | |||
) | |||
witnessHeaders, witnessValidators, chainKeys := genMockNodeWithKeys(chainID, latestHeight, valSize, 2, bTime) | |||
witness := mockp.New(chainID, witnessHeaders, witnessValidators) | |||
forgedKeys := chainKeys[divergenceHeight-1].ChangeKeys(3) // we change 3 out of the 5 validators (still 2/5 remain) | |||
forgedVals := forgedKeys.ToValidators(2, 0) | |||
for height := int64(1); height <= latestHeight; height++ { | |||
if height < divergenceHeight { | |||
primaryHeaders[height] = witnessHeaders[height] | |||
primaryValidators[height] = witnessValidators[height] | |||
continue | |||
} | |||
primaryHeaders[height] = forgedKeys.GenSignedHeader(chainID, height, bTime.Add(time.Duration(height)*time.Minute), | |||
nil, forgedVals, forgedVals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(forgedKeys)) | |||
primaryValidators[height] = forgedVals | |||
} | |||
primary := mockp.New(chainID, primaryHeaders, primaryValidators) | |||
c, err := light.NewClient( | |||
chainID, | |||
light.TrustOptions{ | |||
Period: 4 * time.Hour, | |||
Height: 1, | |||
Hash: primaryHeaders[1].Hash(), | |||
}, | |||
primary, | |||
[]provider.Provider{witness}, | |||
dbs.New(dbm.NewMemDB(), chainID), | |||
light.Logger(log.TestingLogger()), | |||
light.MaxRetryAttempts(1), | |||
) | |||
require.NoError(t, err) | |||
// Check verification returns an error. | |||
_, err = c.VerifyLightBlockAtHeight(10, bTime.Add(1*time.Hour)) | |||
if assert.Error(t, err) { | |||
assert.Contains(t, err.Error(), "does not match primary") | |||
} | |||
// Check evidence was sent to both full nodes. | |||
evAgainstPrimary := &types.LightClientAttackEvidence{ | |||
// after the divergence height the valset doesn't change so we expect the evidence to be for height 10 | |||
ConflictingBlock: &types.LightBlock{ | |||
SignedHeader: primaryHeaders[10], | |||
ValidatorSet: primaryValidators[10], | |||
}, | |||
CommonHeight: 4, | |||
} | |||
assert.True(t, witness.HasEvidence(evAgainstPrimary)) | |||
evAgainstWitness := &types.LightClientAttackEvidence{ | |||
// when forming evidence against witness we learn that the canonical chain continued to change validator sets | |||
// hence the conflicting block is at 7 | |||
ConflictingBlock: &types.LightBlock{ | |||
SignedHeader: witnessHeaders[7], | |||
ValidatorSet: witnessValidators[7], | |||
}, | |||
CommonHeight: 4, | |||
} | |||
assert.True(t, primary.HasEvidence(evAgainstWitness)) | |||
} | |||
func TestLightClientAttackEvidence_Equivocation(t *testing.T) { | |||
// primary performs an equivocation attack | |||
var ( | |||
latestHeight = int64(10) | |||
valSize = 5 | |||
divergenceHeight = int64(6) | |||
primaryHeaders = make(map[int64]*types.SignedHeader, latestHeight) | |||
primaryValidators = make(map[int64]*types.ValidatorSet, latestHeight) | |||
) | |||
// validators don't change in this network (however we still use a map just for convenience) | |||
witnessHeaders, witnessValidators, chainKeys := genMockNodeWithKeys(chainID, latestHeight+2, valSize, 2, bTime) | |||
witness := mockp.New(chainID, witnessHeaders, witnessValidators) | |||
for height := int64(1); height <= latestHeight; height++ { | |||
if height < divergenceHeight { | |||
primaryHeaders[height] = witnessHeaders[height] | |||
primaryValidators[height] = witnessValidators[height] | |||
continue | |||
} | |||
// we don't have a network partition so we will make 4/5 (greater than 2/3) malicious and vote again for | |||
// a different block (which we do by adding txs) | |||
primaryHeaders[height] = chainKeys[height].GenSignedHeader(chainID, height, | |||
bTime.Add(time.Duration(height)*time.Minute), []types.Tx{[]byte("abcd")}, | |||
witnessValidators[height], witnessValidators[height+1], hash("app_hash"), | |||
hash("cons_hash"), hash("results_hash"), 0, len(chainKeys[height])-1) | |||
primaryValidators[height] = witnessValidators[height] | |||
} | |||
primary := mockp.New(chainID, primaryHeaders, primaryValidators) | |||
c, err := light.NewClient( | |||
chainID, | |||
light.TrustOptions{ | |||
Period: 4 * time.Hour, | |||
Height: 1, | |||
Hash: primaryHeaders[1].Hash(), | |||
}, | |||
primary, | |||
[]provider.Provider{witness}, | |||
dbs.New(dbm.NewMemDB(), chainID), | |||
light.Logger(log.TestingLogger()), | |||
light.MaxRetryAttempts(1), | |||
) | |||
require.NoError(t, err) | |||
// Check verification returns an error. | |||
_, err = c.VerifyLightBlockAtHeight(10, bTime.Add(1*time.Hour)) | |||
if assert.Error(t, err) { | |||
assert.Contains(t, err.Error(), "does not match primary") | |||
} | |||
// Check evidence was sent to both full nodes. | |||
// Common height should be set to the height of the divergent header in the instance | |||
// of an equivocation attack and the validator sets are the same as what the witness has | |||
evAgainstPrimary := &types.LightClientAttackEvidence{ | |||
ConflictingBlock: &types.LightBlock{ | |||
SignedHeader: primaryHeaders[divergenceHeight], | |||
ValidatorSet: primaryValidators[divergenceHeight], | |||
}, | |||
CommonHeight: divergenceHeight, | |||
} | |||
assert.True(t, witness.HasEvidence(evAgainstPrimary)) | |||
evAgainstWitness := &types.LightClientAttackEvidence{ | |||
ConflictingBlock: &types.LightBlock{ | |||
SignedHeader: witnessHeaders[divergenceHeight], | |||
ValidatorSet: witnessValidators[divergenceHeight], | |||
}, | |||
CommonHeight: divergenceHeight, | |||
} | |||
assert.True(t, primary.HasEvidence(evAgainstWitness)) | |||
} | |||
func TestClientDivergentTraces(t *testing.T) { | |||
primary := mockp.New(genMockNode(chainID, 10, 5, 2, bTime)) | |||
firstBlock, err := primary.LightBlock(1) | |||
require.NoError(t, err) | |||
witness := mockp.New(genMockNode(chainID, 10, 5, 2, bTime)) | |||
c, err := light.NewClient( | |||
chainID, | |||
light.TrustOptions{ | |||
Height: 1, | |||
Hash: firstBlock.Hash(), | |||
Period: 4 * time.Hour, | |||
}, | |||
primary, | |||
[]provider.Provider{witness}, | |||
dbs.New(dbm.NewMemDB(), chainID), | |||
light.Logger(log.TestingLogger()), | |||
light.MaxRetryAttempts(1), | |||
) | |||
require.NoError(t, err) | |||
// 1. Different nodes therefore a divergent header is produced but the | |||
// light client can't verify it because it has a different trusted header. | |||
_, err = c.VerifyLightBlockAtHeight(10, bTime.Add(1*time.Hour)) | |||
assert.Error(t, err) | |||
assert.Equal(t, 0, len(c.Witnesses())) | |||
// 2. Two out of three nodes don't respond but the third has a header that matches | |||
// verification should be successful and all the witnesses should remain | |||
c, err = light.NewClient( | |||
chainID, | |||
light.TrustOptions{ | |||
Height: 1, | |||
Hash: firstBlock.Hash(), | |||
Period: 4 * time.Hour, | |||
}, | |||
primary, | |||
[]provider.Provider{deadNode, deadNode, primary}, | |||
dbs.New(dbm.NewMemDB(), chainID), | |||
light.Logger(log.TestingLogger()), | |||
light.MaxRetryAttempts(1), | |||
) | |||
require.NoError(t, err) | |||
_, err = c.VerifyLightBlockAtHeight(10, bTime.Add(1*time.Hour)) | |||
assert.NoError(t, err) | |||
assert.Equal(t, 3, len(c.Witnesses())) | |||
} |
@ -0,0 +1,205 @@ | |||
// Code generated by mockery v2.1.0. DO NOT EDIT. | |||
package mocks | |||
import ( | |||
mock "github.com/stretchr/testify/mock" | |||
state "github.com/tendermint/tendermint/state" | |||
tendermintstate "github.com/tendermint/tendermint/proto/tendermint/state" | |||
tenderminttypes "github.com/tendermint/tendermint/types" | |||
types "github.com/tendermint/tendermint/proto/tendermint/types" | |||
) | |||
// Store is an autogenerated mock type for the Store type | |||
type Store struct { | |||
mock.Mock | |||
} | |||
// Bootstrap provides a mock function with given fields: _a0 | |||
func (_m *Store) Bootstrap(_a0 state.State) error { | |||
ret := _m.Called(_a0) | |||
var r0 error | |||
if rf, ok := ret.Get(0).(func(state.State) error); ok { | |||
r0 = rf(_a0) | |||
} else { | |||
r0 = ret.Error(0) | |||
} | |||
return r0 | |||
} | |||
// Load provides a mock function with given fields: | |||
func (_m *Store) Load() (state.State, error) { | |||
ret := _m.Called() | |||
var r0 state.State | |||
if rf, ok := ret.Get(0).(func() state.State); ok { | |||
r0 = rf() | |||
} else { | |||
r0 = ret.Get(0).(state.State) | |||
} | |||
var r1 error | |||
if rf, ok := ret.Get(1).(func() error); ok { | |||
r1 = rf() | |||
} else { | |||
r1 = ret.Error(1) | |||
} | |||
return r0, r1 | |||
} | |||
// LoadABCIResponses provides a mock function with given fields: _a0 | |||
func (_m *Store) LoadABCIResponses(_a0 int64) (*tendermintstate.ABCIResponses, error) { | |||
ret := _m.Called(_a0) | |||
var r0 *tendermintstate.ABCIResponses | |||
if rf, ok := ret.Get(0).(func(int64) *tendermintstate.ABCIResponses); ok { | |||
r0 = rf(_a0) | |||
} else { | |||
if ret.Get(0) != nil { | |||
r0 = ret.Get(0).(*tendermintstate.ABCIResponses) | |||
} | |||
} | |||
var r1 error | |||
if rf, ok := ret.Get(1).(func(int64) error); ok { | |||
r1 = rf(_a0) | |||
} else { | |||
r1 = ret.Error(1) | |||
} | |||
return r0, r1 | |||
} | |||
// LoadConsensusParams provides a mock function with given fields: _a0 | |||
func (_m *Store) LoadConsensusParams(_a0 int64) (types.ConsensusParams, error) { | |||
ret := _m.Called(_a0) | |||
var r0 types.ConsensusParams | |||
if rf, ok := ret.Get(0).(func(int64) types.ConsensusParams); ok { | |||
r0 = rf(_a0) | |||
} else { | |||
r0 = ret.Get(0).(types.ConsensusParams) | |||
} | |||
var r1 error | |||
if rf, ok := ret.Get(1).(func(int64) error); ok { | |||
r1 = rf(_a0) | |||
} else { | |||
r1 = ret.Error(1) | |||
} | |||
return r0, r1 | |||
} | |||
// LoadFromDBOrGenesisDoc provides a mock function with given fields: _a0 | |||
func (_m *Store) LoadFromDBOrGenesisDoc(_a0 *tenderminttypes.GenesisDoc) (state.State, error) { | |||
ret := _m.Called(_a0) | |||
var r0 state.State | |||
if rf, ok := ret.Get(0).(func(*tenderminttypes.GenesisDoc) state.State); ok { | |||
r0 = rf(_a0) | |||
} else { | |||
r0 = ret.Get(0).(state.State) | |||
} | |||
var r1 error | |||
if rf, ok := ret.Get(1).(func(*tenderminttypes.GenesisDoc) error); ok { | |||
r1 = rf(_a0) | |||
} else { | |||
r1 = ret.Error(1) | |||
} | |||
return r0, r1 | |||
} | |||
// LoadFromDBOrGenesisFile provides a mock function with given fields: _a0 | |||
func (_m *Store) LoadFromDBOrGenesisFile(_a0 string) (state.State, error) { | |||
ret := _m.Called(_a0) | |||
var r0 state.State | |||
if rf, ok := ret.Get(0).(func(string) state.State); ok { | |||
r0 = rf(_a0) | |||
} else { | |||
r0 = ret.Get(0).(state.State) | |||
} | |||
var r1 error | |||
if rf, ok := ret.Get(1).(func(string) error); ok { | |||
r1 = rf(_a0) | |||
} else { | |||
r1 = ret.Error(1) | |||
} | |||
return r0, r1 | |||
} | |||
// LoadValidators provides a mock function with given fields: _a0 | |||
func (_m *Store) LoadValidators(_a0 int64) (*tenderminttypes.ValidatorSet, error) { | |||
ret := _m.Called(_a0) | |||
var r0 *tenderminttypes.ValidatorSet | |||
if rf, ok := ret.Get(0).(func(int64) *tenderminttypes.ValidatorSet); ok { | |||
r0 = rf(_a0) | |||
} else { | |||
if ret.Get(0) != nil { | |||
r0 = ret.Get(0).(*tenderminttypes.ValidatorSet) | |||
} | |||
} | |||
var r1 error | |||
if rf, ok := ret.Get(1).(func(int64) error); ok { | |||
r1 = rf(_a0) | |||
} else { | |||
r1 = ret.Error(1) | |||
} | |||
return r0, r1 | |||
} | |||
// PruneStates provides a mock function with given fields: _a0, _a1 | |||
func (_m *Store) PruneStates(_a0 int64, _a1 int64) error { | |||
ret := _m.Called(_a0, _a1) | |||
var r0 error | |||
if rf, ok := ret.Get(0).(func(int64, int64) error); ok { | |||
r0 = rf(_a0, _a1) | |||
} else { | |||
r0 = ret.Error(0) | |||
} | |||
return r0 | |||
} | |||
// Save provides a mock function with given fields: _a0 | |||
func (_m *Store) Save(_a0 state.State) error { | |||
ret := _m.Called(_a0) | |||
var r0 error | |||
if rf, ok := ret.Get(0).(func(state.State) error); ok { | |||
r0 = rf(_a0) | |||
} else { | |||
r0 = ret.Error(0) | |||
} | |||
return r0 | |||
} | |||
// SaveABCIResponses provides a mock function with given fields: _a0, _a1 | |||
func (_m *Store) SaveABCIResponses(_a0 int64, _a1 *tendermintstate.ABCIResponses) error { | |||
ret := _m.Called(_a0, _a1) | |||
var r0 error | |||
if rf, ok := ret.Get(0).(func(int64, *tendermintstate.ABCIResponses) error); ok { | |||
r0 = rf(_a0, _a1) | |||
} else { | |||
r0 = ret.Error(0) | |||
} | |||
return r0 | |||
} |