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 | package evidence | ||||
import ( | import ( | ||||
"bytes" | |||||
"errors" | |||||
"fmt" | "fmt" | ||||
"time" | |||||
sm "github.com/tendermint/tendermint/state" | |||||
"github.com/tendermint/tendermint/light" | |||||
"github.com/tendermint/tendermint/types" | "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 sufficiently recent (MaxAge) | ||||
// - it is from a key who was a validator at the given height | // - 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 ( | var ( | ||||
state = evpool.State() | |||||
height = state.LastBlockHeight | height = state.LastBlockHeight | ||||
evidenceParams = state.ConsensusParams.Evidence | 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 { | 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 from height %d (created at: %v) is too old; min height is %d and evidence can not be older than %v", | ||||
evidence.Height(), | evidence.Height(), | ||||
evidence.Time(), | |||||
evTime, | |||||
height-evidenceParams.MaxAgeNumBlocks, | height-evidenceParams.MaxAgeNumBlocks, | ||||
state.LastBlockTime.Add(evidenceParams.MaxAgeDuration), | 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 { | 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 | 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 ( | import ( | ||||
"fmt" | |||||
"testing" | "testing" | ||||
"time" | "time" | ||||
"github.com/stretchr/testify/assert" | "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/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/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 := &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 := &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() | 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 := &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 | |||||
} |