From 41c11ad2c170851d0881be2560b4c39319ef98c1 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 22 Apr 2020 11:29:05 +0400 Subject: [PATCH] evidence: handling evidence from light client(s) (#4532) Closes: #4530 This PR contains logic for both submitting an evidence by the light client (lite2 package) and receiving it on the Tendermint side (/broadcast_evidence RPC and/or EvidenceReactor#Receive). Upon receiving the ConflictingHeadersEvidence (introduced by this PR), the Tendermint validates it, then breaks it down into smaller pieces (DuplicateVoteEvidence, LunaticValidatorEvidence, PhantomValidatorEvidence, PotentialAmnesiaEvidence). Afterwards, each piece of evidence is verified against the state of the full node and added to the pool, from which it's reaped upon block creation. * rpc/client: do not pass height param if height ptr is nil * rpc/core: validate incoming evidence! * only accept ConflictingHeadersEvidence if one of the headers is committed from this full node's perspective This simplifies the code. Plus, if there are multiple forks, we'll likely to receive multiple ConflictingHeadersEvidence anyway. * swap CommitSig with Vote in LunaticValidatorEvidence Vote is needed to validate signature * no need to embed client http is a provider and should not be used as a client --- CHANGELOG_PENDING.md | 2 + abci/types/types.proto | 12 +- ...047-handling-evidence-from-light-client.md | 44 +- evidence/pool.go | 228 +++++- evidence/pool_test.go | 219 ++++-- evidence/reactor_test.go | 33 +- evidence/store_test.go | 49 +- lite2/client.go | 25 +- lite2/client_test.go | 40 + lite2/errors.go | 19 +- lite2/provider/http/http.go | 44 +- lite2/provider/http/http_test.go | 9 +- lite2/provider/mock/deadmock.go | 17 +- lite2/provider/mock/mock.go | 42 +- lite2/provider/provider.go | 3 + lite2/verifier_test.go | 3 +- node/node.go | 9 +- node/node_test.go | 7 +- rpc/client/evidence_test.go | 210 +++++ rpc/client/http/http.go | 71 +- rpc/client/interface.go | 8 + rpc/client/rpc_test.go | 155 +--- rpc/core/blocks.go | 5 +- rpc/core/evidence.go | 6 + state/store.go | 5 +- state/validation.go | 56 +- types/block.go | 41 +- types/evidence.go | 741 ++++++++++++++++-- types/evidence_test.go | 195 +++++ types/protobuf.go | 9 +- types/vote.go | 2 +- 31 files changed, 1833 insertions(+), 476 deletions(-) create mode 100644 rpc/client/evidence_test.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 2f02d180f..497615df0 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -18,6 +18,8 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi ### FEATURES: +- [evidence] [\#4532](https://github.com/tendermint/tendermint/pull/4532) Handle evidence from light clients (@melekes) +- [lite2] [\#4532](https://github.com/tendermint/tendermint/pull/4532) Submit conflicting headers, if any, to a full node & all witnesses (@melekes) ### IMPROVEMENTS: diff --git a/abci/types/types.proto b/abci/types/types.proto index 351329de1..2d2d3563e 100644 --- a/abci/types/types.proto +++ b/abci/types/types.proto @@ -171,7 +171,7 @@ message ResponseQuery { message ResponseBeginBlock { repeated Event events = 1 - [(gogoproto.nullable) = false, (gogoproto.jsontag) = "events,omitempty"]; + [(gogoproto.nullable) = false, (gogoproto.jsontag) = "events,omitempty"]; } message ResponseCheckTx { @@ -182,7 +182,7 @@ message ResponseCheckTx { int64 gas_wanted = 5; int64 gas_used = 6; repeated Event events = 7 - [(gogoproto.nullable) = false, (gogoproto.jsontag) = "events,omitempty"]; + [(gogoproto.nullable) = false, (gogoproto.jsontag) = "events,omitempty"]; string codespace = 8; } @@ -194,7 +194,7 @@ message ResponseDeliverTx { int64 gas_wanted = 5; int64 gas_used = 6; repeated Event events = 7 - [(gogoproto.nullable) = false, (gogoproto.jsontag) = "events,omitempty"]; + [(gogoproto.nullable) = false, (gogoproto.jsontag) = "events,omitempty"]; string codespace = 8; } @@ -202,7 +202,7 @@ message ResponseEndBlock { repeated ValidatorUpdate validator_updates = 1 [(gogoproto.nullable) = false]; ConsensusParams consensus_param_updates = 2; repeated Event events = 3 - [(gogoproto.nullable) = false, (gogoproto.jsontag) = "events,omitempty"]; + [(gogoproto.nullable) = false, (gogoproto.jsontag) = "events,omitempty"]; } message ResponseCommit { @@ -234,7 +234,7 @@ message EvidenceParams { // Note: must be greater than 0 int64 max_age_num_blocks = 1; google.protobuf.Duration max_age_duration = 2 - [(gogoproto.nullable) = false, (gogoproto.stdduration) = true]; + [(gogoproto.nullable) = false, (gogoproto.stdduration) = true]; } // ValidatorParams contains limits on validators. @@ -250,7 +250,7 @@ message LastCommitInfo { message Event { string type = 1; repeated tendermint.libs.kv.Pair attributes = 2 - [(gogoproto.nullable) = false, (gogoproto.jsontag) = "attributes,omitempty"]; + [(gogoproto.nullable) = false, (gogoproto.jsontag) = "attributes,omitempty"]; } //---------------------------------------- diff --git a/docs/architecture/adr-047-handling-evidence-from-light-client.md b/docs/architecture/adr-047-handling-evidence-from-light-client.md index 8b3a850ba..0f2566fd0 100644 --- a/docs/architecture/adr-047-handling-evidence-from-light-client.md +++ b/docs/architecture/adr-047-handling-evidence-from-light-client.md @@ -3,6 +3,7 @@ ## Changelog * 18-02-2020: Initial draft * 24-02-2020: Second version +* 13-04-2020: Add PotentialAmnesiaEvidence and a few remarks ## Context @@ -26,6 +27,11 @@ type ConflictingHeadersEvidence struct { } ``` +_Remark_: Theoretically, only the header, which differs from what a full node +has, needs to be sent. But sending two headers a) makes evidence easily +verifiable b) simplifies the light client, which does not have query each +witness as to which header it possesses. + When a full node receives the `ConflictingHeadersEvidence` evidence, it should a) validate it b) figure out if malicious behaviour is obvious (immediately slashable) or the fork accountability protocol needs to be started. @@ -34,7 +40,7 @@ slashable) or the fork accountability protocol needs to be started. Check both headers are valid (`ValidateBasic`), have the same height, and signed by 1/3+ of the validator set that the full node had at height -`H1.Height-1`. +`H1.Height`. - Q: What if light client validator set is not equal to full node's validator set (i.e. from full node's point of view both headers are not properly signed; @@ -53,6 +59,9 @@ signed by 1/3+ of the validator set that the full node had at height ### Figuring out if malicious behaviour is immediately slashable Let's say H1 was committed from this full node's perspective (see Appendix A). +_If neither of the headers (H1 and H2) were committed from the full node's +perspective, the evidence must be rejected._ + Intersect validator sets of H1 and H2. * if there are signers(H2) that are not part of validators(H1), they misbehaved as @@ -99,20 +108,23 @@ A new type of evidence needs to be created: ```go type PhantomValidatorEvidence struct { - PubKey crypto.PubKey - Vote types.Vote + Header types.Header + Vote types.Vote + LastHeightValidatorWasInSet int64 } ``` It contains a validator's public key and a vote for a block, where this -validator is not part of the validator set. +validator is not part of the validator set. `LastHeightValidatorWasInSet` +indicates the last height validator was in the validator set. ### F5. Lunatic validator ```go type LunaticValidatorEvidence struct { - Header types.Header - Vote types.Vote + Header types.Header + Vote types.Vote + InvalidHeaderField string } ``` @@ -154,6 +166,26 @@ This includes `ValidatorsHash`, `NextValidatorsHash`, `ConsensusHash`, for the block that was actually committed at the corresponding height, and should thus be easy to check. +`InvalidHeaderField` contains the invalid field name. Note it's very likely +that multiple fields diverge, but it's faster to check just one. This field +MUST NOT be used to determine equality of `LunaticValidatorEvidence`. + +### F2. Amnesia + +```go +type PotentialAmnesiaEvidence struct { + VoteA types.Vote + VoteB types.Vote +} +``` + +To punish this attack, votes under question needs to be sent. Fork +accountability process should then use this evidence to request additional +information from offended validators and construct a new type of evidence to +punish those who conducted an amnesia attack. + +See ADR-056 for the architecture of the fork accountability procedure. + ## Status Proposed. diff --git a/evidence/pool.go b/evidence/pool.go index 68967ede1..446234fb8 100644 --- a/evidence/pool.go +++ b/evidence/pool.go @@ -10,11 +10,11 @@ import ( clist "github.com/tendermint/tendermint/libs/clist" "github.com/tendermint/tendermint/libs/log" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" ) -// Pool maintains a pool of valid evidence -// in an Store. +// Pool maintains a pool of valid evidence in an Store. type Pool struct { logger log.Logger @@ -22,23 +22,43 @@ type Pool struct { evidenceList *clist.CList // concurrent linked-list of evidence // needed to load validators to verify evidence - stateDB dbm.DB + stateDB dbm.DB + blockStore *store.BlockStore + + // a map of active validators and respective last heights validator is active + // if it was in validator set after EvidenceParams.MaxAgeNumBlocks or + // currently is (ie. [MaxAgeNumBlocks, CurrentHeight]) + // In simple words, it means it's still bonded -> therefore slashable. + valToLastHeight valToLastHeightMap // latest state mtx sync.Mutex state sm.State } -func NewPool(stateDB, evidenceDB dbm.DB) *Pool { - store := NewStore(evidenceDB) - evpool := &Pool{ - stateDB: stateDB, - state: sm.LoadState(stateDB), - logger: log.NewNopLogger(), - store: store, - evidenceList: clist.New(), +// Validator.Address -> Last height it was in validator set +type valToLastHeightMap map[string]int64 + +func NewPool(stateDB, evidenceDB dbm.DB, blockStore *store.BlockStore) (*Pool, error) { + var ( + store = NewStore(evidenceDB) + state = sm.LoadState(stateDB) + ) + + valToLastHeight, err := buildValToLastHeightMap(state, stateDB, blockStore) + if err != nil { + return nil, err } - return evpool + + return &Pool{ + stateDB: stateDB, + blockStore: blockStore, + state: state, + logger: log.NewNopLogger(), + store: store, + evidenceList: clist.New(), + valToLastHeight: valToLastHeight, + }, nil } func (evpool *Pool) EvidenceFront() *clist.CElement { @@ -74,7 +94,6 @@ func (evpool *Pool) State() sm.State { // Update loads the latest func (evpool *Pool) Update(block *types.Block, state sm.State) { - // sanity check if state.LastBlockHeight != block.Height { panic( @@ -92,43 +111,81 @@ func (evpool *Pool) Update(block *types.Block, state sm.State) { // remove evidence from pending and mark committed evpool.MarkEvidenceAsCommitted(block.Height, block.Time, block.Evidence.Evidence) + + evpool.updateValToLastHeight(block.Height, state) } -// AddEvidence checks the evidence is valid and adds it to the pool. +// AddEvidence checks the evidence is valid and adds it to the pool. If +// evidence is composite (ConflictingHeadersEvidence), it will be broken up +// into smaller pieces. func (evpool *Pool) AddEvidence(evidence types.Evidence) error { + var ( + state = evpool.State() + evList = []types.Evidence{evidence} + ) - // check if evidence is already stored - if evpool.store.Has(evidence) { - return ErrEvidenceAlreadyStored{} - } - - if err := sm.VerifyEvidence(evpool.stateDB, evpool.State(), evidence); err != nil { - return ErrInvalidEvidence{err} - } - - // fetch the validator and return its voting power as its priority - // TODO: something better ? - valset, err := sm.LoadValidators(evpool.stateDB, evidence.Height()) + valSet, err := sm.LoadValidators(evpool.stateDB, evidence.Height()) if err != nil { - return err - } - _, val := valset.GetByAddress(evidence.Address()) - priority := val.VotingPower - - _, err = evpool.store.AddNewEvidence(evidence, priority) - if err != nil { - return err + return fmt.Errorf("can't load validators at height #%d: %w", evidence.Height(), err) } - evpool.logger.Info("Verified new evidence of byzantine behaviour", "evidence", evidence) + // Break composite evidence into smaller pieces. + if ce, ok := evidence.(types.CompositeEvidence); ok { + evpool.logger.Info("Breaking up composite evidence", "ev", evidence) - // add evidence to clist - evpool.evidenceList.PushBack(evidence) + blockMeta := evpool.blockStore.LoadBlockMeta(evidence.Height()) + if blockMeta == nil { + return fmt.Errorf("don't have block meta at height #%d", evidence.Height()) + } + + if err := ce.VerifyComposite(&blockMeta.Header, valSet); err != nil { + return err + } + + evList = ce.Split(&blockMeta.Header, valSet, evpool.valToLastHeight) + } + + for _, ev := range evList { + if evpool.store.Has(evidence) { + return ErrEvidenceAlreadyStored{} + } + + // For lunatic validator evidence, a header needs to be fetched. + var header *types.Header + if _, ok := ev.(*types.LunaticValidatorEvidence); ok { + blockMeta := evpool.blockStore.LoadBlockMeta(ev.Height()) + if blockMeta == nil { + return fmt.Errorf("don't have block meta at height #%d", ev.Height()) + } + header = &blockMeta.Header + } + + // 1) Verify against state. + if err := sm.VerifyEvidence(evpool.stateDB, state, ev, header); err != nil { + return fmt.Errorf("failed to verify %v: %w", ev, err) + } + + // 2) Compute priority. + _, val := valSet.GetByAddress(ev.Address()) + priority := val.VotingPower + + // 3) Save to store. + _, err := evpool.store.AddNewEvidence(ev, priority) + if err != nil { + return fmt.Errorf("failed to add new evidence %v: %w", ev, err) + } + + // 4) Add evidence to clist. + evpool.evidenceList.PushBack(ev) + + evpool.logger.Info("Verified new evidence of byzantine behaviour", "evidence", ev) + } return nil } -// MarkEvidenceAsCommitted marks all the evidence as committed and removes it from the queue. +// MarkEvidenceAsCommitted marks all the evidence as committed and removes it +// from the queue. func (evpool *Pool) MarkEvidenceAsCommitted(height int64, lastBlockTime time.Time, evidence []types.Evidence) { // make a map of committed evidence to remove from the clist blockEvidenceMap := make(map[string]struct{}) @@ -142,12 +199,25 @@ func (evpool *Pool) MarkEvidenceAsCommitted(height int64, lastBlockTime time.Tim evpool.removeEvidence(height, lastBlockTime, evidenceParams, blockEvidenceMap) } -// IsCommitted returns true if we have already seen this exact evidence and it is already marked as committed. +// IsCommitted returns true if we have already seen this exact evidence and it +// is already marked as committed. func (evpool *Pool) IsCommitted(evidence types.Evidence) bool { ei := evpool.store.getInfo(evidence) return ei.Evidence != nil && ei.Committed } +// ValidatorLastHeight returns the last height of the validator w/ the +// given address. 0 - if address never was a validator or was such a +// long time ago (> ConsensusParams.Evidence.MaxAgeDuration && > +// ConsensusParams.Evidence.MaxAgeNumBlocks). +func (evpool *Pool) ValidatorLastHeight(address []byte) int64 { + h, ok := evpool.valToLastHeight[string(address)] + if !ok { + return 0 + } + return h +} + func (evpool *Pool) removeEvidence( height int64, lastBlockTime time.Time, @@ -174,3 +244,83 @@ func (evpool *Pool) removeEvidence( func evMapKey(ev types.Evidence) string { return string(ev.Hash()) } + +func (evpool *Pool) updateValToLastHeight(blockHeight int64, state sm.State) { + // Update current validators & add new ones. + for _, val := range state.Validators.Validators { + evpool.valToLastHeight[string(val.Address)] = blockHeight + } + + // Remove validators outside of MaxAgeNumBlocks & MaxAgeDuration. + removeHeight := blockHeight - evpool.State().ConsensusParams.Evidence.MaxAgeNumBlocks + if removeHeight >= 1 { + valSet, err := sm.LoadValidators(evpool.stateDB, removeHeight) + if err != nil { + for _, val := range valSet.Validators { + h, ok := evpool.valToLastHeight[string(val.Address)] + if ok && h == removeHeight { + delete(evpool.valToLastHeight, string(val.Address)) + } + } + } + } +} + +func buildValToLastHeightMap(state sm.State, stateDB dbm.DB, blockStore *store.BlockStore) (valToLastHeightMap, error) { + var ( + valToLastHeight = make(map[string]int64) + params = state.ConsensusParams.Evidence + + numBlocks = int64(0) + minAgeTime = time.Now().Add(-params.MaxAgeDuration) + height = state.LastBlockHeight + ) + + if height == 0 { + return valToLastHeight, nil + } + + meta := blockStore.LoadBlockMeta(height) + if meta == nil { + return nil, fmt.Errorf("block meta for height %d not found", height) + } + blockTime := meta.Header.Time + + // From state.LastBlockHeight, build a map of "active" validators until + // MaxAgeNumBlocks is passed and block time is less than now() - + // MaxAgeDuration. + for height >= 1 && (numBlocks <= params.MaxAgeNumBlocks || !blockTime.Before(minAgeTime)) { + valSet, err := sm.LoadValidators(stateDB, height) + if err != nil { + // last stored height -> return + if _, ok := err.(sm.ErrNoValSetForHeight); ok { + return valToLastHeight, nil + } + return nil, fmt.Errorf("validator set for height %d not found", height) + } + + for _, val := range valSet.Validators { + key := string(val.Address) + if _, ok := valToLastHeight[key]; !ok { + valToLastHeight[key] = height + } + } + + height-- + + if height > 0 { + // NOTE: we assume here blockStore and state.Validators are in sync. I.e if + // block N is stored, then validators for height N are also stored in + // state. + meta := blockStore.LoadBlockMeta(height) + if meta == nil { + return nil, fmt.Errorf("block meta for height %d not found", height) + } + blockTime = meta.Header.Time + } + + numBlocks++ + } + + return valToLastHeight, nil +} diff --git a/evidence/pool_test.go b/evidence/pool_test.go index 97694d1ff..cac44bc4f 100644 --- a/evidence/pool_test.go +++ b/evidence/pool_test.go @@ -2,87 +2,65 @@ package evidence import ( "os" - "sync" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" dbm "github.com/tendermint/tm-db" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" ) func TestMain(m *testing.M) { - types.RegisterMockEvidences(cdc) + RegisterMockEvidences() code := m.Run() os.Exit(code) } -func initializeValidatorState(valAddr []byte, height int64) dbm.DB { - stateDB := dbm.NewMemDB() - - // create validator set and state - valSet := &types.ValidatorSet{ - Validators: []*types.Validator{ - {Address: valAddr}, - }, - } - state := sm.State{ - LastBlockHeight: 0, - LastBlockTime: tmtime.Now(), - Validators: valSet, - NextValidators: valSet.CopyIncrementProposerPriority(1), - LastHeightValidatorsChanged: 1, - ConsensusParams: types.ConsensusParams{ - Evidence: types.EvidenceParams{ - MaxAgeNumBlocks: 10000, - MaxAgeDuration: 48 * time.Hour, - }, - }, - } - - // save all states up to height - for i := int64(0); i < height; i++ { - state.LastBlockHeight = i - sm.SaveState(stateDB, state) - } - - return stateDB -} - func TestEvidencePool(t *testing.T) { - var ( valAddr = []byte("val1") - height = int64(100002) + height = int64(52) stateDB = initializeValidatorState(valAddr, height) evidenceDB = dbm.NewMemDB() - pool = NewPool(stateDB, evidenceDB) + blockStoreDB = dbm.NewMemDB() + blockStore = initializeBlockStore(blockStoreDB, sm.LoadState(stateDB), valAddr) evidenceTime = time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC) + + goodEvidence = types.NewMockEvidence(height, time.Now(), 0, valAddr) + badEvidence = types.NewMockEvidence(1, evidenceTime, 0, valAddr) ) - goodEvidence := types.NewMockEvidence(height, time.Now(), 0, valAddr) - badEvidence := types.NewMockEvidence(1, evidenceTime, 0, valAddr) + pool, err := NewPool(stateDB, evidenceDB, blockStore) + require.NoError(t, err) // bad evidence - err := pool.AddEvidence(badEvidence) - assert.Error(t, err) - // err: evidence created at 2019-01-01 00:00:00 +0000 UTC has expired. Evidence can not be older than: ... + err = pool.AddEvidence(badEvidence) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "is too old; min height is 32 and evidence can not be older than") + } - var wg sync.WaitGroup - wg.Add(1) + // good evidence + evAdded := make(chan struct{}) go func() { <-pool.EvidenceWaitChan() - wg.Done() + close(evAdded) }() err = pool.AddEvidence(goodEvidence) - assert.NoError(t, err) - wg.Wait() + require.NoError(t, err) + + select { + case <-evAdded: + case <-time.After(5 * time.Second): + t.Fatal("evidence was not added after 5s") + } assert.Equal(t, 1, pool.evidenceList.Len()) @@ -93,16 +71,19 @@ func TestEvidencePool(t *testing.T) { } func TestEvidencePoolIsCommitted(t *testing.T) { - // Initialization: var ( valAddr = []byte("validator_address") - height = int64(42) + height = int64(1) lastBlockTime = time.Now() stateDB = initializeValidatorState(valAddr, height) evidenceDB = dbm.NewMemDB() - pool = NewPool(stateDB, evidenceDB) + blockStoreDB = dbm.NewMemDB() + blockStore = initializeBlockStore(blockStoreDB, sm.LoadState(stateDB), valAddr) ) + pool, err := NewPool(stateDB, evidenceDB, blockStore) + require.NoError(t, err) + // evidence not seen yet: evidence := types.NewMockEvidence(height, time.Now(), 0, valAddr) assert.False(t, pool.IsCommitted(evidence)) @@ -116,17 +97,20 @@ func TestEvidencePoolIsCommitted(t *testing.T) { assert.True(t, pool.IsCommitted(evidence)) } -func TestAddEvidence(t *testing.T) { - +func TestEvidencePoolAddEvidence(t *testing.T) { var ( valAddr = []byte("val1") - height = int64(100002) + height = int64(30) stateDB = initializeValidatorState(valAddr, height) evidenceDB = dbm.NewMemDB() - pool = NewPool(stateDB, evidenceDB) + blockStoreDB = dbm.NewMemDB() + blockStore = initializeBlockStore(blockStoreDB, sm.LoadState(stateDB), valAddr) evidenceTime = time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC) ) + pool, err := NewPool(stateDB, evidenceDB, blockStore) + require.NoError(t, err) + testCases := []struct { evHeight int64 evTime time.Time @@ -142,10 +126,127 @@ func TestAddEvidence(t *testing.T) { for _, tc := range testCases { tc := tc - ev := types.NewMockEvidence(tc.evHeight, tc.evTime, 0, valAddr) - err := pool.AddEvidence(ev) - if tc.expErr { - assert.Error(t, err) - } + t.Run(tc.evDescription, func(t *testing.T) { + ev := types.NewMockEvidence(tc.evHeight, tc.evTime, 0, valAddr) + err := pool.AddEvidence(ev) + if tc.expErr { + assert.Error(t, err) + t.Log(err) + } + }) } } + +func TestEvidencePoolUpdate(t *testing.T) { + var ( + valAddr = []byte("validator_address") + height = int64(1) + stateDB = initializeValidatorState(valAddr, height) + evidenceDB = dbm.NewMemDB() + blockStoreDB = dbm.NewMemDB() + state = sm.LoadState(stateDB) + blockStore = initializeBlockStore(blockStoreDB, state, valAddr) + ) + + pool, err := NewPool(stateDB, evidenceDB, blockStore) + require.NoError(t, err) + + // create new block (no need to save it to blockStore) + evidence := types.NewMockEvidence(height, time.Now(), 0, valAddr) + lastCommit := makeCommit(height, valAddr) + block := types.MakeBlock(height+1, []types.Tx{}, lastCommit, []types.Evidence{evidence}) + // update state (partially) + state.LastBlockHeight = height + 1 + + pool.Update(block, state) + + // a) Update marks evidence as committed + assert.True(t, pool.IsCommitted(evidence)) + // b) Update updates valToLastHeight map + assert.Equal(t, height+1, pool.ValidatorLastHeight(valAddr)) +} + +func TestEvidencePoolNewPool(t *testing.T) { + var ( + valAddr = []byte("validator_address") + height = int64(1) + stateDB = initializeValidatorState(valAddr, height) + evidenceDB = dbm.NewMemDB() + blockStoreDB = dbm.NewMemDB() + state = sm.LoadState(stateDB) + blockStore = initializeBlockStore(blockStoreDB, state, valAddr) + ) + + pool, err := NewPool(stateDB, evidenceDB, blockStore) + require.NoError(t, err) + + assert.Equal(t, height, pool.ValidatorLastHeight(valAddr)) + assert.EqualValues(t, 0, pool.ValidatorLastHeight([]byte("non-existent-validator"))) +} + +func initializeValidatorState(valAddr []byte, height int64) dbm.DB { + stateDB := dbm.NewMemDB() + + // create validator set and state + valSet := &types.ValidatorSet{ + Validators: []*types.Validator{ + {Address: valAddr, VotingPower: 0}, + }, + } + state := sm.State{ + LastBlockHeight: height, + LastBlockTime: tmtime.Now(), + LastValidators: valSet, + Validators: valSet, + NextValidators: valSet.CopyIncrementProposerPriority(1), + LastHeightValidatorsChanged: 1, + ConsensusParams: types.ConsensusParams{ + Block: types.BlockParams{ + MaxBytes: 22020096, + MaxGas: -1, + }, + Evidence: types.EvidenceParams{ + MaxAgeNumBlocks: 20, + MaxAgeDuration: 48 * time.Hour, + }, + }, + } + + // save all states up to height + for i := int64(0); i <= height; i++ { + state.LastBlockHeight = i + sm.SaveState(stateDB, state) + } + + return stateDB +} + +// initializeBlockStore creates a block storage and populates it w/ a dummy +// block at +height+. +func initializeBlockStore(db dbm.DB, state sm.State, valAddr []byte) *store.BlockStore { + blockStore := store.NewBlockStore(db) + + for i := int64(1); i <= state.LastBlockHeight; i++ { + lastCommit := makeCommit(i-1, valAddr) + block, _ := state.MakeBlock(i, []types.Tx{}, lastCommit, nil, + state.Validators.GetProposer().Address) + + const parts = 1 + partSet := block.MakePartSet(parts) + + seenCommit := makeCommit(i, valAddr) + blockStore.SaveBlock(block, partSet, seenCommit) + } + + return blockStore +} + +func makeCommit(height int64, valAddr []byte) *types.Commit { + commitSigs := []types.CommitSig{{ + BlockIDFlag: types.BlockIDFlagCommit, + ValidatorAddress: valAddr, + Timestamp: time.Now(), + Signature: []byte("Signature"), + }} + return types.NewCommit(height, 0, types.BlockID{}, commitSigs) +} diff --git a/evidence/reactor_test.go b/evidence/reactor_test.go index 135c191da..10d190857 100644 --- a/evidence/reactor_test.go +++ b/evidence/reactor_test.go @@ -8,6 +8,7 @@ import ( "github.com/go-kit/kit/log/term" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" dbm "github.com/tendermint/tm-db" @@ -15,6 +16,7 @@ import ( "github.com/tendermint/tendermint/crypto/secp256k1" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/p2p" + sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) @@ -34,12 +36,18 @@ func evidenceLogger() log.Logger { // connect N evidence reactors through N switches func makeAndConnectReactors(config *cfg.Config, stateDBs []dbm.DB) []*Reactor { N := len(stateDBs) + reactors := make([]*Reactor, N) logger := evidenceLogger() - for i := 0; i < N; i++ { + for i := 0; i < N; i++ { evidenceDB := dbm.NewMemDB() - pool := NewPool(stateDBs[i], evidenceDB) + blockStoreDB := dbm.NewMemDB() + blockStore := initializeBlockStore(blockStoreDB, sm.LoadState(stateDBs[i]), []byte("myval")) + pool, err := NewPool(stateDBs[i], evidenceDB, blockStore) + if err != nil { + panic(err) + } reactors[i] = NewReactor(pool) reactors[i].SetLogger(logger.With("validator", i)) } @@ -49,6 +57,7 @@ func makeAndConnectReactors(config *cfg.Config, stateDBs []dbm.DB) []*Reactor { return s }, p2p.Connect2Switches) + return reactors } @@ -67,7 +76,7 @@ func waitForEvidence(t *testing.T, evs types.EvidenceList, reactors []*Reactor) close(done) }() - timer := time.After(Timeout) + timer := time.After(timeout) select { case <-timer: t.Fatal("Timed out waiting for evidence") @@ -109,15 +118,15 @@ func sendEvidence(t *testing.T, evpool *Pool, valAddr []byte, n int) types.Evide for i := 0; i < n; i++ { ev := types.NewMockEvidence(int64(i+1), time.Now().UTC(), 0, valAddr) err := evpool.AddEvidence(ev) - assert.Nil(t, err) + require.NoError(t, err) evList[i] = ev } return evList } var ( - NumEvidence = 10 - Timeout = 120 * time.Second // ridiculously high because CircleCI is slow + numEvidence = 10 + timeout = 120 * time.Second // ridiculously high because CircleCI is slow ) func TestReactorBroadcastEvidence(t *testing.T) { @@ -128,7 +137,7 @@ func TestReactorBroadcastEvidence(t *testing.T) { stateDBs := make([]dbm.DB, N) valAddr := []byte("myval") // we need validators saved for heights at least as high as we have evidence for - height := int64(NumEvidence) + 10 + height := int64(numEvidence) + 10 for i := 0; i < N; i++ { stateDBs[i] = initializeValidatorState(valAddr, height) } @@ -146,7 +155,7 @@ func TestReactorBroadcastEvidence(t *testing.T) { // send a bunch of valid evidence to the first reactor's evpool // and wait for them all to be received in the others - evList := sendEvidence(t, reactors[0].evpool, valAddr, NumEvidence) + evList := sendEvidence(t, reactors[0].evpool, valAddr, numEvidence) waitForEvidence(t, evList, reactors) } @@ -162,8 +171,8 @@ func TestReactorSelectiveBroadcast(t *testing.T) { config := cfg.TestConfig() valAddr := []byte("myval") - height1 := int64(NumEvidence) + 10 - height2 := int64(NumEvidence) / 2 + height1 := int64(numEvidence) + 10 + height2 := int64(numEvidence) / 2 // DB1 is ahead of DB2 stateDB1 := initializeValidatorState(valAddr, height1) @@ -186,10 +195,10 @@ func TestReactorSelectiveBroadcast(t *testing.T) { peer.Set(types.PeerStateKey, ps) // send a bunch of valid evidence to the first reactor's evpool - evList := sendEvidence(t, reactors[0].evpool, valAddr, NumEvidence) + evList := sendEvidence(t, reactors[0].evpool, valAddr, numEvidence) // only ones less than the peers height should make it through - waitForEvidence(t, evList[:NumEvidence/2], reactors[1:2]) + waitForEvidence(t, evList[:numEvidence/2], reactors[1:2]) // peers should still be connected peers := reactors[1].Switch.Peers().List() diff --git a/evidence/store_test.go b/evidence/store_test.go index 1d45f09a1..91e9a93d0 100644 --- a/evidence/store_test.go +++ b/evidence/store_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/types" @@ -14,8 +15,6 @@ import ( //------------------------------------------- func TestStoreAddDuplicate(t *testing.T) { - assert := assert.New(t) - db := dbm.NewMemDB() store := NewStore(db) @@ -24,17 +23,15 @@ func TestStoreAddDuplicate(t *testing.T) { added, err := store.AddNewEvidence(ev, priority) require.NoError(t, err) - assert.True(added) + assert.True(t, added) // cant add twice added, err = store.AddNewEvidence(ev, priority) require.NoError(t, err) - assert.False(added) + assert.False(t, added) } func TestStoreCommitDuplicate(t *testing.T) { - assert := assert.New(t) - db := dbm.NewMemDB() store := NewStore(db) @@ -45,65 +42,61 @@ func TestStoreCommitDuplicate(t *testing.T) { added, err := store.AddNewEvidence(ev, priority) require.NoError(t, err) - assert.False(added) + assert.False(t, added) } func TestStoreMark(t *testing.T) { - assert := assert.New(t) - db := dbm.NewMemDB() store := NewStore(db) // before we do anything, priority/pending are empty priorityEv := store.PriorityEvidence() pendingEv := store.PendingEvidence(-1) - assert.Equal(0, len(priorityEv)) - assert.Equal(0, len(pendingEv)) + assert.Equal(t, 0, len(priorityEv)) + assert.Equal(t, 0, len(pendingEv)) priority := int64(10) ev := types.NewMockEvidence(2, time.Now().UTC(), 1, []byte("val1")) added, err := store.AddNewEvidence(ev, priority) require.NoError(t, err) - assert.True(added) + assert.True(t, added) // get the evidence. verify. should be uncommitted ei := store.GetInfo(ev.Height(), ev.Hash()) - assert.Equal(ev, ei.Evidence) - assert.Equal(priority, ei.Priority) - assert.False(ei.Committed) + assert.Equal(t, ev, ei.Evidence) + assert.Equal(t, priority, ei.Priority) + assert.False(t, ei.Committed) // new evidence should be returns in priority/pending priorityEv = store.PriorityEvidence() pendingEv = store.PendingEvidence(-1) - assert.Equal(1, len(priorityEv)) - assert.Equal(1, len(pendingEv)) + assert.Equal(t, 1, len(priorityEv)) + assert.Equal(t, 1, len(pendingEv)) // priority is now empty store.MarkEvidenceAsBroadcasted(ev) priorityEv = store.PriorityEvidence() pendingEv = store.PendingEvidence(-1) - assert.Equal(0, len(priorityEv)) - assert.Equal(1, len(pendingEv)) + assert.Equal(t, 0, len(priorityEv)) + assert.Equal(t, 1, len(pendingEv)) // priority and pending are now empty store.MarkEvidenceAsCommitted(ev) priorityEv = store.PriorityEvidence() pendingEv = store.PendingEvidence(-1) - assert.Equal(0, len(priorityEv)) - assert.Equal(0, len(pendingEv)) + assert.Equal(t, 0, len(priorityEv)) + assert.Equal(t, 0, len(pendingEv)) // evidence should show committed newPriority := int64(0) ei = store.GetInfo(ev.Height(), ev.Hash()) - assert.Equal(ev, ei.Evidence) - assert.Equal(newPriority, ei.Priority) - assert.True(ei.Committed) + assert.Equal(t, ev, ei.Evidence) + assert.Equal(t, newPriority, ei.Priority) + assert.True(t, ei.Committed) } func TestStorePriority(t *testing.T) { - assert := assert.New(t) - db := dbm.NewMemDB() store := NewStore(db) @@ -123,11 +116,11 @@ func TestStorePriority(t *testing.T) { for _, c := range cases { added, err := store.AddNewEvidence(c.ev, c.priority) require.NoError(t, err) - assert.True(added) + assert.True(t, added) } evList := store.PriorityEvidence() for i, ev := range evList { - assert.Equal(ev, cases[i].ev) + assert.Equal(t, ev, cases[i].ev) } } diff --git a/lite2/client.go b/lite2/client.go index 6fcb7d173..8d93c2c58 100644 --- a/lite2/client.go +++ b/lite2/client.go @@ -961,11 +961,9 @@ func (c *Client) compareNewHeaderWithWitnesses(h *types.SignedHeader) error { continue } - // TODO: send the diverged headers to primary && all witnesses + c.sendConflictingHeadersEvidence(types.ConflictingHeadersEvidence{H1: h, H2: altH}) - return fmt.Errorf( - "header hash %X does not match one %X from the witness %v", - h.Hash(), altH.Hash(), witness) + return ErrConflictingHeaders{H1: h, Primary: c.primary, H2: altH, Witness: witness} } headerMatched = true @@ -1102,6 +1100,25 @@ func (c *Client) validatorSetFromPrimary(height int64) (*types.ValidatorSet, err return c.validatorSetFromPrimary(height) } +// sendConflictingHeadersEvidence sends evidence to all witnesses and primary +// on best effort basis. +// +// Evidence needs to be submitted to all full nodes since there's no way to +// determine which full node is correct (honest). +func (c *Client) sendConflictingHeadersEvidence(ev types.ConflictingHeadersEvidence) { + err := c.primary.ReportEvidence(ev) + if err != nil { + c.logger.Error("Failed to report evidence to primary", "ev", ev, "primary", c.primary) + } + + for _, w := range c.witnesses { + err := w.ReportEvidence(ev) + if err != nil { + c.logger.Error("Failed to report evidence to witness", "ev", ev, "witness", w) + } + } +} + // exponential backoff (with jitter) // 0.5s -> 2s -> 4.5s -> 8s -> 12.5 with 1s variation func backoffTimeout(attempt uint16) time.Duration { diff --git a/lite2/client_test.go b/lite2/client_test.go index a54786ab7..ae2f9b401 100644 --- a/lite2/client_test.go +++ b/lite2/client_test.go @@ -909,3 +909,43 @@ func TestClientTrustedValidatorSet(t *testing.T) { assert.NotNil(t, valSet) assert.EqualValues(t, 2, height) } + +func TestClientReportsConflictingHeadersEvidence(t *testing.T) { + // fullNode2 sends us different header + altH2 := keys.GenSignedHeaderLastBlockID(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals, + []byte("app_hash2"), []byte("cons_hash"), []byte("results_hash"), + 0, len(keys), types.BlockID{Hash: h1.Hash()}) + fullNode2 := mockp.New( + chainID, + map[int64]*types.SignedHeader{ + 1: h1, + 2: altH2, + }, + map[int64]*types.ValidatorSet{ + 1: vals, + 2: vals, + }, + ) + + c, err := lite.NewClient( + chainID, + trustOptions, + fullNode, + []provider.Provider{fullNode2}, + dbs.New(dbm.NewMemDB(), chainID), + lite.Logger(log.TestingLogger()), + lite.MaxRetryAttempts(1), + ) + require.NoError(t, err) + + // Check verification returns an error. + _, err = c.VerifyHeaderAtHeight(2, bTime.Add(2*time.Hour)) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "does not match one") + } + + // Check evidence was sent to both full nodes. + ev := types.ConflictingHeadersEvidence{H1: h2, H2: altH2} + assert.True(t, fullNode2.HasEvidence(ev)) + assert.True(t, fullNode.HasEvidence(ev)) +} diff --git a/lite2/errors.go b/lite2/errors.go index 7bc70f698..4779a46ec 100644 --- a/lite2/errors.go +++ b/lite2/errors.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/tendermint/tendermint/lite2/provider" "github.com/tendermint/tendermint/types" ) @@ -39,10 +40,26 @@ func (e ErrInvalidHeader) Error() string { return fmt.Sprintf("invalid header: %v", e.Reason) } +// ErrConflictingHeaders is thrown when two conflicting headers are discovered. +type ErrConflictingHeaders struct { + H1 *types.SignedHeader + Primary provider.Provider + + H2 *types.SignedHeader + Witness provider.Provider +} + +func (e ErrConflictingHeaders) Error() string { + return fmt.Sprintf( + "header hash %X from primary %v does not match one %X from witness %v", + e.H1.Hash(), e.Primary, + e.H2.Hash(), e.Witness) +} + // errNoWitnesses means that there are not enough witnesses connected to // continue running the light client. type errNoWitnesses struct{} func (e errNoWitnesses) Error() string { - return fmt.Sprint("no witnesses connected. please reset light client") + return "no witnesses connected. please reset light client" } diff --git a/lite2/provider/http/http.go b/lite2/provider/http/http.go index a7d8534b4..30fdaf2e8 100644 --- a/lite2/provider/http/http.go +++ b/lite2/provider/http/http.go @@ -11,25 +11,17 @@ import ( "github.com/tendermint/tendermint/types" ) -// SignStatusClient combines a SignClient and StatusClient. -type SignStatusClient interface { - rpcclient.SignClient - rpcclient.StatusClient - // Remote returns the remote network address in a string form. - Remote() string -} - -// http provider uses an RPC client (or SignStatusClient more generally) to -// obtain the necessary information. +// http provider uses an RPC client to obtain the necessary information. type http struct { - SignStatusClient // embed so interface can be converted to SignStatusClient for tests - chainID string + chainID string + client rpcclient.RemoteClient } -// New creates a HTTP provider, which is using the rpchttp.HTTP client under the -// hood. If no scheme is provided in the remote URL, http will be used by default. +// New creates a HTTP provider, which is using the rpchttp.HTTP client under +// the hood. If no scheme is provided in the remote URL, http will be used by +// default. func New(chainID, remote string) (provider.Provider, error) { - // ensure URL scheme is set (default HTTP) when not provided + // Ensure URL scheme is set (default HTTP) when not provided. if !strings.Contains(remote, "://") { remote = "http://" + remote } @@ -42,11 +34,11 @@ func New(chainID, remote string) (provider.Provider, error) { return NewWithClient(chainID, httpClient), nil } -// NewWithClient allows you to provide custom SignStatusClient. -func NewWithClient(chainID string, client SignStatusClient) provider.Provider { +// NewWithClient allows you to provide a custom client. +func NewWithClient(chainID string, client rpcclient.RemoteClient) provider.Provider { return &http{ - SignStatusClient: client, - chainID: chainID, + client: client, + chainID: chainID, } } @@ -56,7 +48,7 @@ func (p *http) ChainID() string { } func (p *http) String() string { - return fmt.Sprintf("http{%s}", p.Remote()) + return fmt.Sprintf("http{%s}", p.client.Remote()) } // SignedHeader fetches a SignedHeader at the given height and checks the @@ -67,7 +59,7 @@ func (p *http) SignedHeader(height int64) (*types.SignedHeader, error) { return nil, err } - commit, err := p.SignStatusClient.Commit(h) + commit, err := p.client.Commit(h) if err != nil { // TODO: standartise errors on the RPC side if strings.Contains(err.Error(), "height must be less than or equal") { @@ -97,7 +89,7 @@ func (p *http) ValidatorSet(height int64) (*types.ValidatorSet, error) { } const maxPerPage = 100 - res, err := p.SignStatusClient.Validators(h, 0, maxPerPage) + res, err := p.client.Validators(h, 0, maxPerPage) if err != nil { // TODO: standartise errors on the RPC side if strings.Contains(err.Error(), "height must be less than or equal") { @@ -113,7 +105,7 @@ func (p *http) ValidatorSet(height int64) (*types.ValidatorSet, error) { // Check if there are more validators. for len(res.Validators) == maxPerPage { - res, err = p.SignStatusClient.Validators(h, page, maxPerPage) + res, err = p.client.Validators(h, page, maxPerPage) if err != nil { return nil, err } @@ -126,6 +118,12 @@ func (p *http) ValidatorSet(height int64) (*types.ValidatorSet, error) { return types.NewValidatorSet(vals), nil } +// ReportEvidence calls `/broadcast_evidence` endpoint. +func (p *http) ReportEvidence(ev types.Evidence) error { + _, err := p.client.BroadcastEvidence(ev) + return err +} + func validateHeight(height int64) (*int64, error) { if height < 0 { return nil, fmt.Errorf("expected height >= 0, got height %d", height) diff --git a/lite2/provider/http/http_test.go b/lite2/provider/http/http_test.go index b07dbb9ff..49c3d3622 100644 --- a/lite2/provider/http/http_test.go +++ b/lite2/provider/http/http_test.go @@ -12,6 +12,7 @@ import ( "github.com/tendermint/tendermint/lite2/provider/http" litehttp "github.com/tendermint/tendermint/lite2/provider/http" rpcclient "github.com/tendermint/tendermint/rpc/client" + rpchttp "github.com/tendermint/tendermint/rpc/client/http" rpctest "github.com/tendermint/tendermint/rpc/test" "github.com/tendermint/tendermint/types" ) @@ -50,12 +51,16 @@ func TestProvider(t *testing.T) { } chainID := genDoc.ChainID t.Log("chainID:", chainID) - p, err := litehttp.New(chainID, rpcAddr) + + c, err := rpchttp.New(rpcAddr, "/websocket") + require.Nil(t, err) + + p := litehttp.NewWithClient(chainID, c) require.Nil(t, err) require.NotNil(t, p) // let it produce some blocks - err = rpcclient.WaitForHeight(p.(rpcclient.StatusClient), 6, nil) + err = rpcclient.WaitForHeight(c, 6, nil) require.Nil(t, err) // let's get the highest block diff --git a/lite2/provider/mock/deadmock.go b/lite2/provider/mock/deadmock.go index 77c474411..03e22570e 100644 --- a/lite2/provider/mock/deadmock.go +++ b/lite2/provider/mock/deadmock.go @@ -7,6 +7,8 @@ import ( "github.com/tendermint/tendermint/types" ) +var errNoResp = errors.New("no response from provider") + type deadMock struct { chainID string } @@ -16,18 +18,17 @@ func NewDeadMock(chainID string) provider.Provider { return &deadMock{chainID: chainID} } -func (p *deadMock) ChainID() string { - return p.chainID -} +func (p *deadMock) ChainID() string { return p.chainID } -func (p *deadMock) String() string { - return "deadMock" -} +func (p *deadMock) String() string { return "deadMock" } func (p *deadMock) SignedHeader(height int64) (*types.SignedHeader, error) { - return nil, errors.New("no response from provider") + return nil, errNoResp } func (p *deadMock) ValidatorSet(height int64) (*types.ValidatorSet, error) { - return nil, errors.New("no response from provider") + return nil, errNoResp +} +func (p *deadMock) ReportEvidence(ev types.Evidence) error { + return errNoResp } diff --git a/lite2/provider/mock/mock.go b/lite2/provider/mock/mock.go index 7ff7bc9a1..f4be90454 100644 --- a/lite2/provider/mock/mock.go +++ b/lite2/provider/mock/mock.go @@ -8,28 +8,32 @@ import ( "github.com/tendermint/tendermint/types" ) -type mock struct { - chainID string - headers map[int64]*types.SignedHeader - vals map[int64]*types.ValidatorSet +type Mock struct { + chainID string + headers map[int64]*types.SignedHeader + vals map[int64]*types.ValidatorSet + evidenceToReport map[string]types.Evidence // hash => evidence } +var _ provider.Provider = (*Mock)(nil) + // New creates a mock provider with the given set of headers and validator // sets. -func New(chainID string, headers map[int64]*types.SignedHeader, vals map[int64]*types.ValidatorSet) provider.Provider { - return &mock{ - chainID: chainID, - headers: headers, - vals: vals, +func New(chainID string, headers map[int64]*types.SignedHeader, vals map[int64]*types.ValidatorSet) *Mock { + return &Mock{ + chainID: chainID, + headers: headers, + vals: vals, + evidenceToReport: make(map[string]types.Evidence), } } // ChainID returns the blockchain ID. -func (p *mock) ChainID() string { +func (p *Mock) ChainID() string { return p.chainID } -func (p *mock) String() string { +func (p *Mock) String() string { var headers strings.Builder for _, h := range p.headers { fmt.Fprintf(&headers, " %d:%X", h.Height, h.Hash()) @@ -40,10 +44,10 @@ func (p *mock) String() string { fmt.Fprintf(&vals, " %X", v.Hash()) } - return fmt.Sprintf("mock{headers: %s, vals: %v}", headers.String(), vals.String()) + return fmt.Sprintf("Mock{headers: %s, vals: %v}", headers.String(), vals.String()) } -func (p *mock) SignedHeader(height int64) (*types.SignedHeader, error) { +func (p *Mock) SignedHeader(height int64) (*types.SignedHeader, error) { if height == 0 && len(p.headers) > 0 { return p.headers[int64(len(p.headers))], nil } @@ -53,7 +57,7 @@ func (p *mock) SignedHeader(height int64) (*types.SignedHeader, error) { return nil, provider.ErrSignedHeaderNotFound } -func (p *mock) ValidatorSet(height int64) (*types.ValidatorSet, error) { +func (p *Mock) ValidatorSet(height int64) (*types.ValidatorSet, error) { if height == 0 && len(p.vals) > 0 { return p.vals[int64(len(p.vals))], nil } @@ -62,3 +66,13 @@ func (p *mock) ValidatorSet(height int64) (*types.ValidatorSet, error) { } return nil, provider.ErrValidatorSetNotFound } + +func (p *Mock) ReportEvidence(ev types.Evidence) error { + p.evidenceToReport[string(ev.Hash())] = ev + return nil +} + +func (p *Mock) HasEvidence(ev types.Evidence) bool { + _, ok := p.evidenceToReport[string(ev.Hash())] + return ok +} diff --git a/lite2/provider/provider.go b/lite2/provider/provider.go index 773e17e32..02a56f29d 100644 --- a/lite2/provider/provider.go +++ b/lite2/provider/provider.go @@ -32,4 +32,7 @@ type Provider interface { // If there's no ValidatorSet for the given height, ErrValidatorSetNotFound // error is returned. ValidatorSet(height int64) (*types.ValidatorSet, error) + + // ReportEvidence reports an evidence of misbehavior. + ReportEvidence(ev types.Evidence) error } diff --git a/lite2/verifier_test.go b/lite2/verifier_test.go index 5a207321d..db2129b21 100644 --- a/lite2/verifier_test.go +++ b/lite2/verifier_test.go @@ -57,8 +57,7 @@ func TestVerifyAdjacentHeaders(t *testing.T) { 3 * time.Hour, bTime.Add(2 * time.Hour), nil, - "untrustedHeader.ValidateBasic failed: signedHeader belongs to another chain 'different-chainID' not" + - " 'TestVerifyAdjacentHeaders'", + "header belongs to another chain", }, // new header's time is before old header's time -> error 2: { diff --git a/node/node.go b/node/node.go index 5fb0664ea..c7d02fe28 100644 --- a/node/node.go +++ b/node/node.go @@ -341,14 +341,17 @@ func createMempoolAndMempoolReactor(config *cfg.Config, proxyApp proxy.AppConns, } func createEvidenceReactor(config *cfg.Config, dbProvider DBProvider, - stateDB dbm.DB, logger log.Logger) (*evidence.Reactor, *evidence.Pool, error) { + stateDB dbm.DB, blockStore *store.BlockStore, logger log.Logger) (*evidence.Reactor, *evidence.Pool, error) { evidenceDB, err := dbProvider(&DBContext{"evidence", config}) if err != nil { return nil, nil, err } evidenceLogger := logger.With("module", "evidence") - evidencePool := evidence.NewPool(stateDB, evidenceDB) + evidencePool, err := evidence.NewPool(stateDB, evidenceDB, blockStore) + if err != nil { + return nil, nil, err + } evidencePool.SetLogger(evidenceLogger) evidenceReactor := evidence.NewReactor(evidencePool) evidenceReactor.SetLogger(evidenceLogger) @@ -639,7 +642,7 @@ func NewNode(config *cfg.Config, mempoolReactor, mempool := createMempoolAndMempoolReactor(config, proxyApp, state, memplMetrics, logger) // Make Evidence Reactor - evidenceReactor, evidencePool, err := createEvidenceReactor(config, dbProvider, stateDB, logger) + evidenceReactor, evidencePool, err := createEvidenceReactor(config, dbProvider, stateDB, blockStore, logger) if err != nil { return nil, err } diff --git a/node/node_test.go b/node/node_test.go index a9a43a362..ef21e3094 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -26,6 +26,7 @@ import ( "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" "github.com/tendermint/tendermint/version" @@ -250,7 +251,9 @@ func TestCreateProposalBlock(t *testing.T) { types.RegisterMockEvidencesGlobal() // XXX! evidence.RegisterMockEvidences() evidenceDB := dbm.NewMemDB() - evidencePool := evidence.NewPool(stateDB, evidenceDB) + blockStore := store.NewBlockStore(dbm.NewMemDB()) + evidencePool, err := evidence.NewPool(stateDB, evidenceDB, blockStore) + require.NoError(t, err) evidencePool.SetLogger(logger) // fill the evidence pool with more evidence @@ -260,7 +263,7 @@ func TestCreateProposalBlock(t *testing.T) { for i := 0; i < numEv; i++ { ev := types.NewMockRandomEvidence(1, time.Now(), proposerAddr, tmrand.Bytes(minEvSize)) err := evidencePool.AddEvidence(ev) - assert.NoError(t, err) + require.NoError(t, err) } // fill the mempool with more txs diff --git a/rpc/client/evidence_test.go b/rpc/client/evidence_test.go new file mode 100644 index 000000000..3bead4592 --- /dev/null +++ b/rpc/client/evidence_test.go @@ -0,0 +1,210 @@ +package client_test + +import ( + "bytes" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/tmhash" + "github.com/tendermint/tendermint/privval" + "github.com/tendermint/tendermint/rpc/client" + rpctest "github.com/tendermint/tendermint/rpc/test" + "github.com/tendermint/tendermint/types" +) + +func newEvidence(t *testing.T, val *privval.FilePV, + vote *types.Vote, vote2 *types.Vote, + chainID string) *types.DuplicateVoteEvidence { + + var err error + vote.Signature, err = val.Key.PrivKey.Sign(vote.SignBytes(chainID)) + require.NoError(t, err) + + vote2.Signature, err = val.Key.PrivKey.Sign(vote2.SignBytes(chainID)) + require.NoError(t, err) + + return types.NewDuplicateVoteEvidence(val.Key.PubKey, vote, vote2) +} + +func makeEvidences( + t *testing.T, + val *privval.FilePV, + chainID string, +) (correct *types.DuplicateVoteEvidence, fakes []*types.DuplicateVoteEvidence) { + vote := types.Vote{ + ValidatorAddress: val.Key.Address, + ValidatorIndex: 0, + Height: 1, + Round: 0, + Type: types.PrevoteType, + Timestamp: time.Now().UTC(), + BlockID: types.BlockID{ + Hash: tmhash.Sum([]byte("blockhash")), + PartsHeader: types.PartSetHeader{ + Total: 1000, + Hash: tmhash.Sum([]byte("partset")), + }, + }, + } + + vote2 := vote + vote2.BlockID.Hash = tmhash.Sum([]byte("blockhash2")) + correct = newEvidence(t, val, &vote, &vote2, chainID) + + fakes = make([]*types.DuplicateVoteEvidence, 0) + + // different address + { + v := vote2 + v.ValidatorAddress = []byte("some_address") + fakes = append(fakes, newEvidence(t, val, &vote, &v, chainID)) + } + + // different index + { + v := vote2 + v.ValidatorIndex = vote.ValidatorIndex + 1 + fakes = append(fakes, newEvidence(t, val, &vote, &v, chainID)) + } + + // different height + { + v := vote2 + v.Height = vote.Height + 1 + fakes = append(fakes, newEvidence(t, val, &vote, &v, chainID)) + } + + // different round + { + v := vote2 + v.Round = vote.Round + 1 + fakes = append(fakes, newEvidence(t, val, &vote, &v, chainID)) + } + + // different type + { + v := vote2 + v.Type = types.PrecommitType + fakes = append(fakes, newEvidence(t, val, &vote, &v, chainID)) + } + + // exactly same vote + { + v := vote + fakes = append(fakes, newEvidence(t, val, &vote, &v, chainID)) + } + + return correct, fakes +} + +func TestBroadcastEvidence_DuplicateVoteEvidence(t *testing.T) { + var ( + config = rpctest.GetConfig() + chainID = config.ChainID() + pv = privval.LoadOrGenFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()) + ) + + correct, fakes := makeEvidences(t, pv, chainID) + + for i, c := range GetClients() { + t.Logf("client %d", i) + + result, err := c.BroadcastEvidence(correct) + require.NoError(t, err, "BroadcastEvidence(%s) failed", correct) + assert.Equal(t, correct.Hash(), result.Hash, "expected result hash to match evidence hash") + + status, err := c.Status() + require.NoError(t, err) + client.WaitForHeight(c, status.SyncInfo.LatestBlockHeight+2, nil) + + ed25519pub := correct.PubKey.(ed25519.PubKeyEd25519) + rawpub := ed25519pub[:] + result2, err := c.ABCIQuery("/val", rawpub) + require.NoError(t, err) + qres := result2.Response + require.True(t, qres.IsOK()) + + var v abci.ValidatorUpdate + err = abci.ReadMessage(bytes.NewReader(qres.Value), &v) + require.NoError(t, err, "Error reading query result, value %v", qres.Value) + + require.EqualValues(t, rawpub, v.PubKey.Data, "Stored PubKey not equal with expected, value %v", string(qres.Value)) + require.Equal(t, int64(9), v.Power, "Stored Power not equal with expected, value %v", string(qres.Value)) + + for _, fake := range fakes { + _, err := c.BroadcastEvidence(fake) + require.Error(t, err, "BroadcastEvidence(%s) succeeded, but the evidence was fake", fake) + } + } +} + +func TestBroadcastEvidence_ConflictingHeadersEvidence(t *testing.T) { + var ( + config = rpctest.GetConfig() + chainID = config.ChainID() + pv = privval.LoadOrGenFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()) + ) + + for i, c := range GetClients() { + t.Logf("client %d", i) + + h1, err := c.Commit(nil) + require.NoError(t, err) + require.NotNil(t, h1.SignedHeader.Header) + + // Create an alternative header with a different AppHash. + h2 := &types.SignedHeader{ + Header: &types.Header{ + Version: h1.Version, + ChainID: h1.ChainID, + Height: h1.Height, + Time: h1.Time, + LastBlockID: h1.LastBlockID, + LastCommitHash: h1.LastCommitHash, + DataHash: h1.DataHash, + ValidatorsHash: h1.ValidatorsHash, + NextValidatorsHash: h1.NextValidatorsHash, + ConsensusHash: h1.ConsensusHash, + AppHash: crypto.CRandBytes(32), + LastResultsHash: h1.LastResultsHash, + EvidenceHash: h1.EvidenceHash, + ProposerAddress: h1.ProposerAddress, + }, + Commit: types.NewCommit(h1.Height, 1, h1.Commit.BlockID, h1.Commit.Signatures), + } + h2.Commit.BlockID = types.BlockID{ + Hash: h2.Hash(), + PartsHeader: types.PartSetHeader{Total: 1, Hash: crypto.CRandBytes(32)}, + } + vote := &types.Vote{ + ValidatorAddress: pv.Key.Address, + ValidatorIndex: 0, + Height: h2.Height, + Round: h2.Commit.Round, + Timestamp: h2.Time, + Type: types.PrecommitType, + BlockID: h2.Commit.BlockID, + } + signBytes, err := pv.Key.PrivKey.Sign(vote.SignBytes(chainID)) + require.NoError(t, err) + h2.Commit.Signatures[0] = types.NewCommitSigForBlock(signBytes, pv.Key.Address, h2.Time) + + t.Logf("h1 AppHash: %X", h1.AppHash) + t.Logf("h2 AppHash: %X", h2.AppHash) + + ev := types.ConflictingHeadersEvidence{ + H1: &h1.SignedHeader, + H2: h2, + } + + result, err := c.BroadcastEvidence(ev) + require.NoError(t, err, "BroadcastEvidence(%s) failed", ev) + assert.Equal(t, ev.Hash(), result.Hash, "expected result hash to match evidence hash") + } +} diff --git a/rpc/client/http/http.go b/rpc/client/http/http.go index 9af0b6cf4..be66d807d 100644 --- a/rpc/client/http/http.go +++ b/rpc/client/http/http.go @@ -210,7 +210,7 @@ func (c *baseRPCClient) Status() (*ctypes.ResultStatus, error) { result := new(ctypes.ResultStatus) _, err := c.caller.Call("status", map[string]interface{}{}, result) if err != nil { - return nil, errors.Wrap(err, "Status") + return nil, err } return result, nil } @@ -219,7 +219,7 @@ func (c *baseRPCClient) ABCIInfo() (*ctypes.ResultABCIInfo, error) { result := new(ctypes.ResultABCIInfo) _, err := c.caller.Call("abci_info", map[string]interface{}{}, result) if err != nil { - return nil, errors.Wrap(err, "ABCIInfo") + return nil, err } return result, nil } @@ -237,7 +237,7 @@ func (c *baseRPCClient) ABCIQueryWithOptions( map[string]interface{}{"path": path, "data": data, "height": opts.Height, "prove": opts.Prove}, result) if err != nil { - return nil, errors.Wrap(err, "ABCIQuery") + return nil, err } return result, nil } @@ -246,7 +246,7 @@ func (c *baseRPCClient) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastT result := new(ctypes.ResultBroadcastTxCommit) _, err := c.caller.Call("broadcast_tx_commit", map[string]interface{}{"tx": tx}, result) if err != nil { - return nil, errors.Wrap(err, "broadcast_tx_commit") + return nil, err } return result, nil } @@ -263,7 +263,7 @@ func (c *baseRPCClient) broadcastTX(route string, tx types.Tx) (*ctypes.ResultBr result := new(ctypes.ResultBroadcastTx) _, err := c.caller.Call(route, map[string]interface{}{"tx": tx}, result) if err != nil { - return nil, errors.Wrap(err, route) + return nil, err } return result, nil } @@ -272,7 +272,7 @@ func (c *baseRPCClient) UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, result := new(ctypes.ResultUnconfirmedTxs) _, err := c.caller.Call("unconfirmed_txs", map[string]interface{}{"limit": limit}, result) if err != nil { - return nil, errors.Wrap(err, "unconfirmed_txs") + return nil, err } return result, nil } @@ -281,7 +281,7 @@ func (c *baseRPCClient) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error result := new(ctypes.ResultUnconfirmedTxs) _, err := c.caller.Call("num_unconfirmed_txs", map[string]interface{}{}, result) if err != nil { - return nil, errors.Wrap(err, "num_unconfirmed_txs") + return nil, err } return result, nil } @@ -290,7 +290,7 @@ func (c *baseRPCClient) NetInfo() (*ctypes.ResultNetInfo, error) { result := new(ctypes.ResultNetInfo) _, err := c.caller.Call("net_info", map[string]interface{}{}, result) if err != nil { - return nil, errors.Wrap(err, "NetInfo") + return nil, err } return result, nil } @@ -299,7 +299,7 @@ func (c *baseRPCClient) DumpConsensusState() (*ctypes.ResultDumpConsensusState, result := new(ctypes.ResultDumpConsensusState) _, err := c.caller.Call("dump_consensus_state", map[string]interface{}{}, result) if err != nil { - return nil, errors.Wrap(err, "DumpConsensusState") + return nil, err } return result, nil } @@ -315,9 +315,13 @@ func (c *baseRPCClient) ConsensusState() (*ctypes.ResultConsensusState, error) { func (c *baseRPCClient) ConsensusParams(height *int64) (*ctypes.ResultConsensusParams, error) { result := new(ctypes.ResultConsensusParams) - _, err := c.caller.Call("consensus_params", map[string]interface{}{"height": height}, result) + params := make(map[string]interface{}) + if height != nil { + params["height"] = height + } + _, err := c.caller.Call("consensus_params", params, result) if err != nil { - return nil, errors.Wrap(err, "ConsensusParams") + return nil, err } return result, nil } @@ -326,7 +330,7 @@ func (c *baseRPCClient) Health() (*ctypes.ResultHealth, error) { result := new(ctypes.ResultHealth) _, err := c.caller.Call("health", map[string]interface{}{}, result) if err != nil { - return nil, errors.Wrap(err, "Health") + return nil, err } return result, nil } @@ -337,7 +341,7 @@ func (c *baseRPCClient) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.Resu map[string]interface{}{"minHeight": minHeight, "maxHeight": maxHeight}, result) if err != nil { - return nil, errors.Wrap(err, "BlockchainInfo") + return nil, err } return result, nil } @@ -346,34 +350,46 @@ func (c *baseRPCClient) Genesis() (*ctypes.ResultGenesis, error) { result := new(ctypes.ResultGenesis) _, err := c.caller.Call("genesis", map[string]interface{}{}, result) if err != nil { - return nil, errors.Wrap(err, "Genesis") + return nil, err } return result, nil } func (c *baseRPCClient) Block(height *int64) (*ctypes.ResultBlock, error) { result := new(ctypes.ResultBlock) - _, err := c.caller.Call("block", map[string]interface{}{"height": height}, result) + params := make(map[string]interface{}) + if height != nil { + params["height"] = height + } + _, err := c.caller.Call("block", params, result) if err != nil { - return nil, errors.Wrap(err, "Block") + return nil, err } return result, nil } func (c *baseRPCClient) BlockResults(height *int64) (*ctypes.ResultBlockResults, error) { result := new(ctypes.ResultBlockResults) - _, err := c.caller.Call("block_results", map[string]interface{}{"height": height}, result) + params := make(map[string]interface{}) + if height != nil { + params["height"] = height + } + _, err := c.caller.Call("block_results", params, result) if err != nil { - return nil, errors.Wrap(err, "Block Result") + return nil, err } return result, nil } func (c *baseRPCClient) Commit(height *int64) (*ctypes.ResultCommit, error) { result := new(ctypes.ResultCommit) - _, err := c.caller.Call("commit", map[string]interface{}{"height": height}, result) + params := make(map[string]interface{}) + if height != nil { + params["height"] = height + } + _, err := c.caller.Call("commit", params, result) if err != nil { - return nil, errors.Wrap(err, "Commit") + return nil, err } return result, nil } @@ -403,20 +419,23 @@ func (c *baseRPCClient) TxSearch(query string, prove bool, page, perPage int, or } _, err := c.caller.Call("tx_search", params, result) if err != nil { - return nil, errors.Wrap(err, "TxSearch") + return nil, err } return result, nil } func (c *baseRPCClient) Validators(height *int64, page, perPage int) (*ctypes.ResultValidators, error) { result := new(ctypes.ResultValidators) - _, err := c.caller.Call("validators", map[string]interface{}{ - "height": height, + params := map[string]interface{}{ "page": page, "per_page": perPage, - }, result) + } + if height != nil { + params["height"] = height + } + _, err := c.caller.Call("validators", params, result) if err != nil { - return nil, errors.Wrap(err, "Validators") + return nil, err } return result, nil } @@ -425,7 +444,7 @@ func (c *baseRPCClient) BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroa result := new(ctypes.ResultBroadcastEvidence) _, err := c.caller.Call("broadcast_evidence", map[string]interface{}{"evidence": ev}, result) if err != nil { - return nil, errors.Wrap(err, "BroadcastEvidence") + return nil, err } return result, nil } diff --git a/rpc/client/interface.go b/rpc/client/interface.go index 408d803c8..f26f27b6f 100644 --- a/rpc/client/interface.go +++ b/rpc/client/interface.go @@ -121,3 +121,11 @@ type MempoolClient interface { type EvidenceClient interface { BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) } + +// RemoteClient is a Client, which can also return the remote network address. +type RemoteClient interface { + Client + + // Remote returns the remote network address in a string form. + Remote() string +} diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index 3f9962774..1038551c6 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -1,27 +1,21 @@ package client_test import ( - "bytes" "fmt" "math" - "math/rand" "net/http" "strings" "sync" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/crypto/ed25519" - "github.com/tendermint/tendermint/crypto/tmhash" "github.com/tendermint/tendermint/libs/log" tmmath "github.com/tendermint/tendermint/libs/math" mempl "github.com/tendermint/tendermint/mempool" - "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/rpc/client" rpchttp "github.com/tendermint/tendermint/rpc/client/http" rpclocal "github.com/tendermint/tendermint/rpc/client/local" @@ -173,7 +167,8 @@ func TestGenesisAndValidators(t *testing.T) { gval := gen.Genesis.Validators[0] // get the current validators - vals, err := c.Validators(nil, 0, 0) + h := int64(1) + vals, err := c.Validators(&h, 0, 0) require.Nil(t, err, "%d: %+v", i, err) require.Equal(t, 1, len(vals.Validators)) require.Equal(t, 1, vals.Count) @@ -554,152 +549,6 @@ func TestTxSearch(t *testing.T) { } } -func deepcpVote(vote *types.Vote) (res *types.Vote) { - res = &types.Vote{ - ValidatorAddress: make([]byte, len(vote.ValidatorAddress)), - ValidatorIndex: vote.ValidatorIndex, - Height: vote.Height, - Round: vote.Round, - Type: vote.Type, - Timestamp: vote.Timestamp, - BlockID: types.BlockID{ - Hash: make([]byte, len(vote.BlockID.Hash)), - PartsHeader: vote.BlockID.PartsHeader, - }, - Signature: make([]byte, len(vote.Signature)), - } - copy(res.ValidatorAddress, vote.ValidatorAddress) - copy(res.BlockID.Hash, vote.BlockID.Hash) - copy(res.Signature, vote.Signature) - return -} - -func newEvidence( - t *testing.T, - val *privval.FilePV, - vote *types.Vote, - vote2 *types.Vote, - chainID string, -) types.DuplicateVoteEvidence { - var err error - deepcpVote2 := deepcpVote(vote2) - deepcpVote2.Signature, err = val.Key.PrivKey.Sign(deepcpVote2.SignBytes(chainID)) - require.NoError(t, err) - - return *types.NewDuplicateVoteEvidence(val.Key.PubKey, vote, deepcpVote2) -} - -func makeEvidences( - t *testing.T, - val *privval.FilePV, - chainID string, -) (ev types.DuplicateVoteEvidence, fakes []types.DuplicateVoteEvidence) { - vote := &types.Vote{ - ValidatorAddress: val.Key.Address, - ValidatorIndex: 0, - Height: 1, - Round: 0, - Type: types.PrevoteType, - Timestamp: time.Now().UTC(), - BlockID: types.BlockID{ - Hash: tmhash.Sum([]byte("blockhash")), - PartsHeader: types.PartSetHeader{ - Total: 1000, - Hash: tmhash.Sum([]byte("partset")), - }, - }, - } - - var err error - vote.Signature, err = val.Key.PrivKey.Sign(vote.SignBytes(chainID)) - require.NoError(t, err) - - vote2 := deepcpVote(vote) - vote2.BlockID.Hash = tmhash.Sum([]byte("blockhash2")) - - ev = newEvidence(t, val, vote, vote2, chainID) - - fakes = make([]types.DuplicateVoteEvidence, 42) - - // different address - vote2 = deepcpVote(vote) - for i := 0; i < 10; i++ { - rand.Read(vote2.ValidatorAddress) // nolint: gosec - fakes[i] = newEvidence(t, val, vote, vote2, chainID) - } - // different index - vote2 = deepcpVote(vote) - for i := 10; i < 20; i++ { - vote2.ValidatorIndex = rand.Int()%100 + 1 // nolint: gosec - fakes[i] = newEvidence(t, val, vote, vote2, chainID) - } - // different height - vote2 = deepcpVote(vote) - for i := 20; i < 30; i++ { - vote2.Height = rand.Int63()%1000 + 100 // nolint: gosec - fakes[i] = newEvidence(t, val, vote, vote2, chainID) - } - // different round - vote2 = deepcpVote(vote) - for i := 30; i < 40; i++ { - vote2.Round = rand.Int()%10 + 1 // nolint: gosec - fakes[i] = newEvidence(t, val, vote, vote2, chainID) - } - // different type - vote2 = deepcpVote(vote) - vote2.Type = types.PrecommitType - fakes[40] = newEvidence(t, val, vote, vote2, chainID) - // exactly same vote - vote2 = deepcpVote(vote) - fakes[41] = newEvidence(t, val, vote, vote2, chainID) - return ev, fakes -} - -func TestBroadcastEvidenceDuplicateVote(t *testing.T) { - config := rpctest.GetConfig() - chainID := config.ChainID() - pvKeyFile := config.PrivValidatorKeyFile() - pvKeyStateFile := config.PrivValidatorStateFile() - pv := privval.LoadOrGenFilePV(pvKeyFile, pvKeyStateFile) - - ev, fakes := makeEvidences(t, pv, chainID) - t.Logf("evidence %v", ev) - - for i, c := range GetClients() { - t.Logf("client %d", i) - - result, err := c.BroadcastEvidence(&ev) - require.Nil(t, err) - require.Equal(t, ev.Hash(), result.Hash, "Invalid response, result %+v", result) - - status, err := c.Status() - require.NoError(t, err) - client.WaitForHeight(c, status.SyncInfo.LatestBlockHeight+2, nil) - - ed25519pub := ev.PubKey.(ed25519.PubKeyEd25519) - rawpub := ed25519pub[:] - result2, err := c.ABCIQuery("/val", rawpub) - require.Nil(t, err, "Error querying evidence, err %v", err) - qres := result2.Response - require.True(t, qres.IsOK(), "Response not OK") - - var v abci.ValidatorUpdate - err = abci.ReadMessage(bytes.NewReader(qres.Value), &v) - require.NoError(t, err, "Error reading query result, value %v", qres.Value) - - require.EqualValues(t, rawpub, v.PubKey.Data, "Stored PubKey not equal with expected, value %v", string(qres.Value)) - require.Equal(t, int64(9), v.Power, "Stored Power not equal with expected, value %v", string(qres.Value)) - - for _, fake := range fakes { - _, err := c.BroadcastEvidence(&types.DuplicateVoteEvidence{ - PubKey: fake.PubKey, - VoteA: fake.VoteA, - VoteB: fake.VoteB}) - require.Error(t, err, "Broadcasting fake evidence succeed: %s", fake.String()) - } - } -} - func TestBatchedJSONRPCCalls(t *testing.T) { c := getHTTPClient() testBatchedJSONRPCCalls(t, c) diff --git a/rpc/core/blocks.go b/rpc/core/blocks.go index 1d608534a..f807bde7c 100644 --- a/rpc/core/blocks.go +++ b/rpc/core/blocks.go @@ -155,10 +155,11 @@ func getHeight(currentBase int64, currentHeight int64, heightPtr *int64) (int64, if heightPtr != nil { height := *heightPtr if height <= 0 { - return 0, fmt.Errorf("height must be greater than 0") + return 0, fmt.Errorf("height must be greater than 0, but got %d", height) } if height > currentHeight { - return 0, fmt.Errorf("height must be less than or equal to the current blockchain height") + return 0, fmt.Errorf("height %d must be less than or equal to the current blockchain height %d", + height, currentHeight) } if height < currentBase { return 0, fmt.Errorf("height %v is not available, blocks pruned at height %v", diff --git a/rpc/core/evidence.go b/rpc/core/evidence.go index 7d7ac2ec7..51eaabc1c 100644 --- a/rpc/core/evidence.go +++ b/rpc/core/evidence.go @@ -1,6 +1,8 @@ package core import ( + "fmt" + "github.com/tendermint/tendermint/evidence" ctypes "github.com/tendermint/tendermint/rpc/core/types" rpctypes "github.com/tendermint/tendermint/rpc/lib/types" @@ -10,6 +12,10 @@ import ( // BroadcastEvidence broadcasts evidence of the misbehavior. // More: https://docs.tendermint.com/master/rpc/#/Info/broadcast_evidence func BroadcastEvidence(ctx *rpctypes.Context, ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { + if err := ev.ValidateBasic(); err != nil { + return nil, fmt.Errorf("evidence.ValidateBasic failed: %w", err) + } + err := evidencePool.AddEvidence(ev) if _, ok := err.(evidence.ErrEvidenceAlreadyStored); err == nil || ok { return &ctypes.ResultBroadcastEvidence{Hash: ev.Hash()}, nil diff --git a/state/store.go b/state/store.go index 08b695f8a..149fd595b 100644 --- a/state/store.go +++ b/state/store.go @@ -111,7 +111,10 @@ func saveState(db dbm.DB, state State, key []byte) { saveValidatorsInfo(db, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators) // Save next consensus params. saveConsensusParamsInfo(db, nextHeight, state.LastHeightConsensusParamsChanged, state.ConsensusParams) - db.SetSync(key, state.Bytes()) + err := db.SetSync(key, state.Bytes()) + if err != nil { + panic(err) + } } //------------------------------------------------------------------------ diff --git a/state/validation.go b/state/validation.go index ccbcc72e2..18608ab2c 100644 --- a/state/validation.go +++ b/state/validation.go @@ -132,7 +132,7 @@ func validateBlock(evidencePool EvidencePool, stateDB dbm.DB, state State, block // Validate all evidence. for _, ev := range block.Evidence.Evidence { - if err := VerifyEvidence(stateDB, state, ev); err != nil { + if err := VerifyEvidence(stateDB, state, ev, &block.Header); err != nil { return types.NewErrEvidenceInvalid(ev, err) } if evidencePool != nil && evidencePool.IsCommitted(ev) { @@ -158,7 +158,7 @@ func validateBlock(evidencePool EvidencePool, stateDB dbm.DB, state State, block // - 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(stateDB dbm.DB, state State, evidence types.Evidence) error { +func VerifyEvidence(stateDB dbm.DB, state State, evidence types.Evidence, committedHeader *types.Header) error { var ( height = state.LastBlockHeight evidenceParams = state.ConsensusParams.Evidence @@ -177,6 +177,12 @@ func VerifyEvidence(stateDB dbm.DB, state State, evidence types.Evidence) error ) } + if ev, ok := evidence.(*types.LunaticValidatorEvidence); ok { + if err := ev.VerifyHeader(committedHeader); err != nil { + return err + } + } + valset, err := LoadValidators(stateDB, evidence.Height()) if err != nil { // TODO: if err is just that we cant find it cuz we pruned, ignore. @@ -184,16 +190,42 @@ func VerifyEvidence(stateDB dbm.DB, state State, evidence types.Evidence) error return err } - // The address must have been an active validator at the height. - // NOTE: we will ignore evidence from H if the key was not a validator - // at H, even if it is a validator at some nearby H' - // XXX: this makes lite-client bisection as is unsafe - // See https://github.com/tendermint/tendermint/issues/3244 - ev := evidence - height, addr := ev.Height(), ev.Address() - _, val := valset.GetByAddress(addr) - if val == nil { - return fmt.Errorf("address %X was not a validator at height %d", addr, height) + addr := evidence.Address() + var val *types.Validator + + // For PhantomValidatorEvidence, check evidence.Address was not part of the + // validator set at height evidence.Height, but was a validator before OR + // after. + if phve, ok := evidence.(*types.PhantomValidatorEvidence); ok { + _, val = valset.GetByAddress(addr) + if val != nil { + return fmt.Errorf("address %X was a validator at height %d", addr, evidence.Height()) + } + + // check if last height validator was in the validator set is within + // MaxAgeNumBlocks. + if ageNumBlocks > 0 && phve.LastHeightValidatorWasInSet <= ageNumBlocks { + return fmt.Errorf("last time validator was in the set at height %d, min: %d", + phve.LastHeightValidatorWasInSet, ageNumBlocks+1) + } + + valset, err := LoadValidators(stateDB, phve.LastHeightValidatorWasInSet) + if err != nil { + // TODO: if err is just that we cant find it cuz we pruned, ignore. + // TODO: if its actually bad evidence, punish peer + return err + } + _, val = valset.GetByAddress(addr) + if val == nil { + return fmt.Errorf("phantom validator %X not found", addr) + } + } else { + // For all other types, expect evidence.Address to be a validator at height + // evidence.Height. + _, val = valset.GetByAddress(addr) + if val == nil { + return fmt.Errorf("address %X was not a validator at height %d", addr, evidence.Height()) + } } if err := evidence.Verify(state.ChainID, val.PubKey); err != nil { diff --git a/types/block.go b/types/block.go index 61ae6719b..6f6fd2ea1 100644 --- a/types/block.go +++ b/types/block.go @@ -752,40 +752,37 @@ type SignedHeader struct { // ValidateBasic does basic consistency checks and makes sure the header // and commit are consistent. -// NOTE: This does not actually check the cryptographic signatures. Make -// sure to use a Verifier to validate the signatures actually provide a +// +// NOTE: This does not actually check the cryptographic signatures. Make sure +// to use a Verifier to validate the signatures actually provide a // significantly strong proof for this header's validity. func (sh SignedHeader) ValidateBasic(chainID string) error { - // Make sure the header is consistent with the commit. if sh.Header == nil { - return errors.New("signedHeader missing header") + return errors.New("missing header") } if sh.Commit == nil { - return errors.New("signedHeader missing commit (precommit votes)") + return errors.New("missing commit (precommit votes)") } - // Check ChainID. + // if err := sh.Header.ValidateBasic(); err != nil { + // return fmt.Errorf("header.ValidateBasic failed: %w", err) + // } + + if err := sh.Commit.ValidateBasic(); err != nil { + return fmt.Errorf("commit.ValidateBasic failed: %w", err) + } + + // Make sure the header is consistent with the commit. if sh.ChainID != chainID { - return fmt.Errorf("signedHeader belongs to another chain '%s' not '%s'", - sh.ChainID, chainID) + return fmt.Errorf("header belongs to another chain %q, not %q", sh.ChainID, chainID) } - // Check Height. if sh.Commit.Height != sh.Height { - return fmt.Errorf("signedHeader header and commit height mismatch: %v vs %v", - sh.Height, sh.Commit.Height) + return fmt.Errorf("header and commit height mismatch: %d vs %d", sh.Height, sh.Commit.Height) } - // Check Hash. - hhash := sh.Hash() - chash := sh.Commit.BlockID.Hash - if !bytes.Equal(hhash, chash) { - return fmt.Errorf("signedHeader commit signs block %X, header is block %X", - chash, hhash) - } - // ValidateBasic on the Commit. - err := sh.Commit.ValidateBasic() - if err != nil { - return errors.Wrap(err, "commit.ValidateBasic failed during SignedHeader.ValidateBasic") + if hhash, chash := sh.Hash(), sh.Commit.BlockID.Hash; !bytes.Equal(hhash, chash) { + return fmt.Errorf("commit signs block %X, header is block %X", chash, hhash) } + return nil } diff --git a/types/evidence.go b/types/evidence.go index 244244f9e..80275677f 100644 --- a/types/evidence.go +++ b/types/evidence.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/tendermint/tendermint/crypto/tmhash" + tmmath "github.com/tendermint/tendermint/libs/math" amino "github.com/tendermint/go-amino" @@ -19,6 +20,14 @@ import ( const ( // MaxEvidenceBytes is a maximum size of any evidence (including amino overhead). MaxEvidenceBytes int64 = 484 + + // An invalid field in the header from LunaticValidatorEvidence. + // Must be a function of the ABCI application state. + ValidatorsHashField = "ValidatorsHash" + NextValidatorsHashField = "NextValidatorsHash" + ConsensusHashField = "ConsensusHash" + AppHashField = "AppHash" + LastResultsHashField = "LastResultsHash" ) // ErrEvidenceInvalid wraps a piece of evidence and the error denoting how or why it is invalid. @@ -55,7 +64,7 @@ func (err *ErrEvidenceOverflow) Error() string { //------------------------------------------- -// Evidence represents any provable malicious activity by a validator +// Evidence represents any provable malicious activity by a validator. type Evidence interface { Height() int64 // height of the equivocation Time() time.Time // time of the equivocation @@ -69,9 +78,18 @@ type Evidence interface { String() string } +type CompositeEvidence interface { + VerifyComposite(committedHeader *Header, valSet *ValidatorSet) error + Split(committedHeader *Header, valSet *ValidatorSet, valToLastHeight map[string]int64) []Evidence +} + func RegisterEvidences(cdc *amino.Codec) { cdc.RegisterInterface((*Evidence)(nil), nil) cdc.RegisterConcrete(&DuplicateVoteEvidence{}, "tendermint/DuplicateVoteEvidence", nil) + cdc.RegisterConcrete(&ConflictingHeadersEvidence{}, "tendermint/ConflictingHeadersEvidence", nil) + cdc.RegisterConcrete(&PhantomValidatorEvidence{}, "tendermint/PhantomValidatorEvidence", nil) + cdc.RegisterConcrete(&LunaticValidatorEvidence{}, "tendermint/LunaticValidatorEvidence", nil) + cdc.RegisterConcrete(&PotentialAmnesiaEvidence{}, "tendermint/PotentialAmnesiaEvidence", nil) } func RegisterMockEvidences(cdc *amino.Codec) { @@ -129,7 +147,7 @@ func NewDuplicateVoteEvidence(pubkey crypto.PubKey, vote1 *Vote, vote2 *Vote) *D // String returns a string representation of the evidence. func (dve *DuplicateVoteEvidence) String() string { - return fmt.Sprintf("VoteA: %v; VoteB: %v", dve.VoteA, dve.VoteB) + return fmt.Sprintf("DuplicateVoteEvidence{VoteA: %v, VoteB: %v}", dve.VoteA, dve.VoteB) } @@ -138,7 +156,7 @@ func (dve *DuplicateVoteEvidence) Height() int64 { return dve.VoteA.Height } -// Time return the time the evidence was created +// Time returns the time the evidence was created. func (dve *DuplicateVoteEvidence) Time() time.Time { return dve.VoteA.Timestamp } @@ -159,19 +177,22 @@ func (dve *DuplicateVoteEvidence) Hash() []byte { } // Verify returns an error if the two votes aren't conflicting. -// To be conflicting, they must be from the same validator, for the same H/R/S, but for different blocks. +// +// To be conflicting, they must be from the same validator, for the same H/R/S, +// but for different blocks. func (dve *DuplicateVoteEvidence) Verify(chainID string, pubKey crypto.PubKey) error { // H/R/S must be the same if dve.VoteA.Height != dve.VoteB.Height || dve.VoteA.Round != dve.VoteB.Round || dve.VoteA.Type != dve.VoteB.Type { - return fmt.Errorf("duplicateVoteEvidence Error: H/R/S does not match. Got %v and %v", dve.VoteA, dve.VoteB) + return fmt.Errorf("h/r/s does not match: %d/%d/%v vs %d/%d/%v", + dve.VoteA.Height, dve.VoteA.Round, dve.VoteA.Type, + dve.VoteB.Height, dve.VoteB.Round, dve.VoteB.Type) } // Address must be the same if !bytes.Equal(dve.VoteA.ValidatorAddress, dve.VoteB.ValidatorAddress) { - return fmt.Errorf( - "duplicateVoteEvidence Error: Validator addresses do not match. Got %X and %X", + return fmt.Errorf("validator addresses do not match: %X vs %X", dve.VoteA.ValidatorAddress, dve.VoteB.ValidatorAddress, ) @@ -180,7 +201,7 @@ func (dve *DuplicateVoteEvidence) Verify(chainID string, pubKey crypto.PubKey) e // Index must be the same if dve.VoteA.ValidatorIndex != dve.VoteB.ValidatorIndex { return fmt.Errorf( - "duplicateVoteEvidence Error: Validator indices do not match. Got %d and %d", + "validator indices do not match: %d and %d", dve.VoteA.ValidatorIndex, dve.VoteB.ValidatorIndex, ) @@ -189,7 +210,7 @@ func (dve *DuplicateVoteEvidence) Verify(chainID string, pubKey crypto.PubKey) e // BlockIDs must be different if dve.VoteA.BlockID.Equals(dve.VoteB.BlockID) { return fmt.Errorf( - "duplicateVoteEvidence Error: BlockIDs are the same (%v) - not a real duplicate vote", + "block IDs are the same (%v) - not a real duplicate vote", dve.VoteA.BlockID, ) } @@ -197,16 +218,16 @@ func (dve *DuplicateVoteEvidence) Verify(chainID string, pubKey crypto.PubKey) e // pubkey must match address (this should already be true, sanity check) addr := dve.VoteA.ValidatorAddress if !bytes.Equal(pubKey.Address(), addr) { - return fmt.Errorf("duplicateVoteEvidence FAILED SANITY CHECK - address (%X) doesn't match pubkey (%v - %X)", + return fmt.Errorf("address (%X) doesn't match pubkey (%v - %X)", addr, pubKey, pubKey.Address()) } // Signatures must be valid if !pubKey.VerifyBytes(dve.VoteA.SignBytes(chainID), dve.VoteA.Signature) { - return fmt.Errorf("duplicateVoteEvidence Error verifying VoteA: %v", ErrVoteInvalidSignature) + return fmt.Errorf("verifying VoteA: %w", ErrVoteInvalidSignature) } if !pubKey.VerifyBytes(dve.VoteB.SignBytes(chainID), dve.VoteB.Signature) { - return fmt.Errorf("duplicateVoteEvidence Error verifying VoteB: %v", ErrVoteInvalidSignature) + return fmt.Errorf("verifying VoteB: %w", ErrVoteInvalidSignature) } return nil @@ -233,10 +254,10 @@ func (dve *DuplicateVoteEvidence) ValidateBasic() error { return fmt.Errorf("one or both of the votes are empty %v, %v", dve.VoteA, dve.VoteB) } if err := dve.VoteA.ValidateBasic(); err != nil { - return fmt.Errorf("invalid VoteA: %v", err) + return fmt.Errorf("invalid VoteA: %w", err) } if err := dve.VoteB.ValidateBasic(); err != nil { - return fmt.Errorf("invalid VoteB: %v", err) + return fmt.Errorf("invalid VoteB: %w", err) } // Enforce Votes are lexicographically sorted on blockID if strings.Compare(dve.VoteA.BlockID.Key(), dve.VoteB.BlockID.Key()) >= 0 { @@ -245,6 +266,663 @@ func (dve *DuplicateVoteEvidence) ValidateBasic() error { return nil } +//------------------------------------------- + +// EvidenceList is a list of Evidence. Evidences is not a word. +type EvidenceList []Evidence + +// Hash returns the simple merkle root hash of the EvidenceList. +func (evl EvidenceList) Hash() []byte { + // These allocations are required because Evidence is not of type Bytes, and + // golang slices can't be typed cast. This shouldn't be a performance problem since + // the Evidence size is capped. + evidenceBzs := make([][]byte, len(evl)) + for i := 0; i < len(evl); i++ { + evidenceBzs[i] = evl[i].Bytes() + } + return merkle.SimpleHashFromByteSlices(evidenceBzs) +} + +func (evl EvidenceList) String() string { + s := "" + for _, e := range evl { + s += fmt.Sprintf("%s\t\t", e) + } + return s +} + +// Has returns true if the evidence is in the EvidenceList. +func (evl EvidenceList) Has(evidence Evidence) bool { + for _, ev := range evl { + if ev.Equal(evidence) { + return true + } + } + return false +} + +//------------------------------------------- + +// ConflictingHeadersEvidence is primarily used by the light client when it +// observes two conflicting headers, both having 1/3+ of the voting power of +// the currently trusted validator set. +type ConflictingHeadersEvidence struct { + H1 *SignedHeader `json:"h_1"` + H2 *SignedHeader `json:"h_2"` +} + +var _ Evidence = &ConflictingHeadersEvidence{} +var _ CompositeEvidence = &ConflictingHeadersEvidence{} +var _ Evidence = ConflictingHeadersEvidence{} +var _ CompositeEvidence = ConflictingHeadersEvidence{} + +// Split breaks up eviddence into smaller chunks (one per validator except for +// PotentialAmnesiaEvidence): PhantomValidatorEvidence, +// LunaticValidatorEvidence, DuplicateVoteEvidence and +// PotentialAmnesiaEvidence. +// +// committedHeader - header at height H1.Height == H2.Height +// valSet - validator set at height H1.Height == H2.Height +// valToLastHeight - map between active validators and respective last heights +func (ev ConflictingHeadersEvidence) Split(committedHeader *Header, valSet *ValidatorSet, + valToLastHeight map[string]int64) []Evidence { + + evList := make([]Evidence, 0) + + var alternativeHeader *SignedHeader + if bytes.Equal(committedHeader.Hash(), ev.H1.Hash()) { + alternativeHeader = ev.H2 + } else { + alternativeHeader = ev.H1 + } + + // If there are signers(alternativeHeader) that are not part of + // validators(committedHeader), they misbehaved as they are signing protocol + // messages in heights they are not validators => immediately slashable + // (#F4). + for i, sig := range alternativeHeader.Commit.Signatures { + if sig.Absent() { + continue + } + + lastHeightValidatorWasInSet, ok := valToLastHeight[string(sig.ValidatorAddress)] + if !ok { + continue + } + + if !valSet.HasAddress(sig.ValidatorAddress) { + evList = append(evList, &PhantomValidatorEvidence{ + Header: alternativeHeader.Header, + Vote: alternativeHeader.Commit.GetVote(i), + LastHeightValidatorWasInSet: lastHeightValidatorWasInSet, + }) + } + } + + // If ValidatorsHash, NextValidatorsHash, ConsensusHash, AppHash, and + // LastResultsHash in alternativeHeader are different (incorrect application + // state transition), then it is a lunatic misbehavior => immediately + // slashable (#F5). + var invalidField string + switch { + case !bytes.Equal(committedHeader.ValidatorsHash, alternativeHeader.ValidatorsHash): + invalidField = "ValidatorsHash" + case !bytes.Equal(committedHeader.NextValidatorsHash, alternativeHeader.NextValidatorsHash): + invalidField = "NextValidatorsHash" + case !bytes.Equal(committedHeader.ConsensusHash, alternativeHeader.ConsensusHash): + invalidField = "ConsensusHash" + case !bytes.Equal(committedHeader.AppHash, alternativeHeader.AppHash): + invalidField = "AppHash" + case !bytes.Equal(committedHeader.LastResultsHash, alternativeHeader.LastResultsHash): + invalidField = "LastResultsHash" + } + if invalidField != "" { + for i, sig := range alternativeHeader.Commit.Signatures { + if sig.Absent() { + continue + } + evList = append(evList, &LunaticValidatorEvidence{ + Header: alternativeHeader.Header, + Vote: alternativeHeader.Commit.GetVote(i), + InvalidHeaderField: invalidField, + }) + } + return evList + } + + // Use the fact that signatures are sorted by ValidatorAddress. + var ( + i = 0 + j = 0 + ) +OUTER_LOOP: + for i < len(ev.H1.Commit.Signatures) { + sigA := ev.H1.Commit.Signatures[i] + if sigA.Absent() { + i++ + continue + } + // FIXME: Replace with HasAddress once DuplicateVoteEvidence#PubKey is + // removed. + _, val := valSet.GetByAddress(sigA.ValidatorAddress) + if val == nil { + i++ + continue + } + + for j < len(ev.H2.Commit.Signatures) { + sigB := ev.H2.Commit.Signatures[j] + if sigB.Absent() { + j++ + continue + } + + switch bytes.Compare(sigA.ValidatorAddress, sigB.ValidatorAddress) { + case 0: + // if H1.Round == H2.Round, and some signers signed different precommit + // messages in both commits, then it is an equivocation misbehavior => + // immediately slashable (#F1). + if ev.H1.Commit.Round == ev.H2.Commit.Round { + evList = append(evList, &DuplicateVoteEvidence{ + PubKey: val.PubKey, + VoteA: ev.H1.Commit.GetVote(i), + VoteB: ev.H2.Commit.GetVote(j), + }) + } else { + // if H1.Round != H2.Round we need to run full detection procedure => not + // immediately slashable. + evList = append(evList, &PotentialAmnesiaEvidence{ + VoteA: ev.H1.Commit.GetVote(i), + VoteB: ev.H2.Commit.GetVote(j), + }) + } + + i++ + j++ + continue OUTER_LOOP + case 1: + i++ + continue OUTER_LOOP + case -1: + j++ + } + } + } + + return evList +} + +func (ev ConflictingHeadersEvidence) Height() int64 { return ev.H1.Height } + +// XXX: this is not the time of equivocation +func (ev ConflictingHeadersEvidence) Time() time.Time { return ev.H1.Time } + +func (ev ConflictingHeadersEvidence) Address() []byte { + panic("use ConflictingHeadersEvidence#Split to split evidence into individual pieces") +} + +func (ev ConflictingHeadersEvidence) Bytes() []byte { + return cdcEncode(ev) +} + +func (ev ConflictingHeadersEvidence) Hash() []byte { + bz := make([]byte, tmhash.Size*2) + copy(bz[:tmhash.Size-1], ev.H1.Hash().Bytes()) + copy(bz[tmhash.Size:], ev.H2.Hash().Bytes()) + return tmhash.Sum(bz) +} + +func (ev ConflictingHeadersEvidence) Verify(chainID string, _ crypto.PubKey) error { + panic("use ConflictingHeadersEvidence#VerifyComposite to verify composite evidence") +} + +// VerifyComposite verifies that both headers belong to the same chain, same +// height and signed by 1/3+ of validators at height H1.Height == H2.Height. +func (ev ConflictingHeadersEvidence) VerifyComposite(committedHeader *Header, valSet *ValidatorSet) error { + var alternativeHeader *SignedHeader + switch { + case bytes.Equal(committedHeader.Hash(), ev.H1.Hash()): + alternativeHeader = ev.H2 + case bytes.Equal(committedHeader.Hash(), ev.H2.Hash()): + alternativeHeader = ev.H1 + default: + return errors.New("none of the headers are committed from this node's perspective") + } + + // ChainID must be the same + if committedHeader.ChainID != alternativeHeader.ChainID { + return errors.New("alt header is from a different chain") + } + + // Height must be the same + if committedHeader.Height != alternativeHeader.Height { + return errors.New("alt header is from a different height") + } + + // Limit the number of signatures to avoid DoS attacks where a header + // contains too many signatures. + // + // Validator set size = 100 [node] + // Max validator set size = 100 * 2 = 200 [fork?] + maxNumValidators := valSet.Size() * 2 + if len(alternativeHeader.Commit.Signatures) > maxNumValidators { + return errors.Errorf("alt commit contains too many signatures: %d, expected no more than %d", + len(alternativeHeader.Commit.Signatures), + maxNumValidators) + } + + // Header must be signed by at least 1/3+ of voting power of currently + // trusted validator set. + if err := valSet.VerifyCommitTrusting( + alternativeHeader.ChainID, + alternativeHeader.Commit.BlockID, + alternativeHeader.Height, + alternativeHeader.Commit, + tmmath.Fraction{Numerator: 1, Denominator: 3}); err != nil { + return errors.Wrap(err, "alt header does not have 1/3+ of voting power of our validator set") + } + + return nil +} + +func (ev ConflictingHeadersEvidence) Equal(ev2 Evidence) bool { + switch e2 := ev2.(type) { + case ConflictingHeadersEvidence: + return bytes.Equal(ev.H1.Hash(), e2.H1.Hash()) && bytes.Equal(ev.H2.Hash(), e2.H2.Hash()) + case *ConflictingHeadersEvidence: + return bytes.Equal(ev.H1.Hash(), e2.H1.Hash()) && bytes.Equal(ev.H2.Hash(), e2.H2.Hash()) + default: + return false + } +} + +func (ev ConflictingHeadersEvidence) ValidateBasic() error { + if ev.H1 == nil { + return errors.New("first header is missing") + } + + if ev.H2 == nil { + return errors.New("second header is missing") + } + + if err := ev.H1.ValidateBasic(ev.H1.ChainID); err != nil { + return fmt.Errorf("h1: %w", err) + } + if err := ev.H2.ValidateBasic(ev.H2.ChainID); err != nil { + return fmt.Errorf("h2: %w", err) + } + return nil +} + +func (ev ConflictingHeadersEvidence) String() string { + return fmt.Sprintf("ConflictingHeadersEvidence{H1: %d#%X, H2: %d#%X}", + ev.H1.Height, ev.H1.Hash(), + ev.H2.Height, ev.H2.Hash()) +} + +//------------------------------------------- + +type PhantomValidatorEvidence struct { + Header *Header `json:"header"` + Vote *Vote `json:"vote"` + LastHeightValidatorWasInSet int64 `json:"last_height_validator_was_in_set"` +} + +var _ Evidence = &PhantomValidatorEvidence{} +var _ Evidence = PhantomValidatorEvidence{} + +func (e PhantomValidatorEvidence) Height() int64 { + return e.Header.Height +} + +func (e PhantomValidatorEvidence) Time() time.Time { + return e.Header.Time +} + +func (e PhantomValidatorEvidence) Address() []byte { + return e.Vote.ValidatorAddress +} + +func (e PhantomValidatorEvidence) Hash() []byte { + bz := make([]byte, tmhash.Size+crypto.AddressSize) + copy(bz[:tmhash.Size-1], e.Header.Hash().Bytes()) + copy(bz[tmhash.Size:], e.Vote.ValidatorAddress.Bytes()) + return tmhash.Sum(bz) +} + +func (e PhantomValidatorEvidence) Bytes() []byte { + return cdcEncode(e) +} + +func (e PhantomValidatorEvidence) Verify(chainID string, pubKey crypto.PubKey) error { + // chainID must be the same + if chainID != e.Header.ChainID { + return fmt.Errorf("chainID do not match: %s vs %s", + chainID, + e.Header.ChainID, + ) + } + + if !pubKey.VerifyBytes(e.Vote.SignBytes(chainID), e.Vote.Signature) { + return errors.New("invalid signature") + } + + return nil +} + +func (e PhantomValidatorEvidence) Equal(ev Evidence) bool { + switch e2 := ev.(type) { + case PhantomValidatorEvidence: + return bytes.Equal(e.Header.Hash(), e2.Header.Hash()) && + bytes.Equal(e.Vote.ValidatorAddress, e2.Vote.ValidatorAddress) + case *PhantomValidatorEvidence: + return bytes.Equal(e.Header.Hash(), e2.Header.Hash()) && + bytes.Equal(e.Vote.ValidatorAddress, e2.Vote.ValidatorAddress) + default: + return false + } +} + +func (e PhantomValidatorEvidence) ValidateBasic() error { + if e.Header == nil { + return errors.New("empty header") + } + + if e.Vote == nil { + return errors.New("empty vote") + } + + // if err := e.Header.ValidateBasic(); err != nil { + // return fmt.Errorf("invalid header: %v", err) + // } + + if err := e.Vote.ValidateBasic(); err != nil { + return fmt.Errorf("invalid signature: %v", err) + } + + if !e.Vote.BlockID.IsComplete() { + return errors.New("expected vote for block") + } + + if e.Header.Height != e.Vote.Height { + return fmt.Errorf("header and vote have different heights: %d vs %d", + e.Header.Height, + e.Vote.Height, + ) + } + + if e.LastHeightValidatorWasInSet <= 0 { + return errors.New("negative or zero LastHeightValidatorWasInSet") + } + + return nil +} + +func (e PhantomValidatorEvidence) String() string { + return fmt.Sprintf("PhantomValidatorEvidence{%X voted for %d/%X}", + e.Vote.ValidatorAddress, e.Header.Height, e.Header.Hash()) +} + +//------------------------------------------- + +type LunaticValidatorEvidence struct { + Header *Header `json:"header"` + Vote *Vote `json:"vote"` + InvalidHeaderField string `json:"invalid_header_field"` +} + +var _ Evidence = &LunaticValidatorEvidence{} +var _ Evidence = LunaticValidatorEvidence{} + +func (e LunaticValidatorEvidence) Height() int64 { + return e.Header.Height +} + +func (e LunaticValidatorEvidence) Time() time.Time { + return e.Header.Time +} + +func (e LunaticValidatorEvidence) Address() []byte { + return e.Vote.ValidatorAddress +} + +func (e LunaticValidatorEvidence) Hash() []byte { + bz := make([]byte, tmhash.Size+crypto.AddressSize) + copy(bz[:tmhash.Size-1], e.Header.Hash().Bytes()) + copy(bz[tmhash.Size:], e.Vote.ValidatorAddress.Bytes()) + return tmhash.Sum(bz) +} + +func (e LunaticValidatorEvidence) Bytes() []byte { + return cdcEncode(e) +} + +func (e LunaticValidatorEvidence) Verify(chainID string, pubKey crypto.PubKey) error { + // chainID must be the same + if chainID != e.Header.ChainID { + return fmt.Errorf("chainID do not match: %s vs %s", + chainID, + e.Header.ChainID, + ) + } + + if !pubKey.VerifyBytes(e.Vote.SignBytes(chainID), e.Vote.Signature) { + return errors.New("invalid signature") + } + + return nil +} + +func (e LunaticValidatorEvidence) Equal(ev Evidence) bool { + switch e2 := ev.(type) { + case LunaticValidatorEvidence: + return bytes.Equal(e.Header.Hash(), e2.Header.Hash()) && + bytes.Equal(e.Vote.ValidatorAddress, e2.Vote.ValidatorAddress) + case *LunaticValidatorEvidence: + return bytes.Equal(e.Header.Hash(), e2.Header.Hash()) && + bytes.Equal(e.Vote.ValidatorAddress, e2.Vote.ValidatorAddress) + default: + return false + } +} + +func (e LunaticValidatorEvidence) ValidateBasic() error { + if e.Header == nil { + return errors.New("empty header") + } + + if e.Vote == nil { + return errors.New("empty vote") + } + + // if err := e.Header.ValidateBasic(); err != nil { + // return fmt.Errorf("invalid header: %v", err) + // } + + if err := e.Vote.ValidateBasic(); err != nil { + return fmt.Errorf("invalid signature: %v", err) + } + + if !e.Vote.BlockID.IsComplete() { + return errors.New("expected vote for block") + } + + if e.Header.Height != e.Vote.Height { + return fmt.Errorf("header and vote have different heights: %d vs %d", + e.Header.Height, + e.Vote.Height, + ) + } + + switch e.InvalidHeaderField { + case "ValidatorsHash", "NextValidatorsHash", "ConsensusHash", "AppHash", "LastResultsHash": + return nil + default: + return errors.New("unknown invalid header field") + } +} + +func (e LunaticValidatorEvidence) String() string { + return fmt.Sprintf("LunaticValidatorEvidence{%X voted for %d/%X, which contains invalid %s}", + e.Vote.ValidatorAddress, e.Header.Height, e.Header.Hash(), e.InvalidHeaderField) +} + +func (e LunaticValidatorEvidence) VerifyHeader(committedHeader *Header) error { + matchErr := func(field string) error { + return fmt.Errorf("%s matches committed hash", field) + } + + switch e.InvalidHeaderField { + case ValidatorsHashField: + if bytes.Equal(committedHeader.ValidatorsHash, e.Header.ValidatorsHash) { + return matchErr(ValidatorsHashField) + } + case NextValidatorsHashField: + if bytes.Equal(committedHeader.NextValidatorsHash, e.Header.NextValidatorsHash) { + return matchErr(NextValidatorsHashField) + } + case ConsensusHashField: + if bytes.Equal(committedHeader.ConsensusHash, e.Header.ConsensusHash) { + return matchErr(ConsensusHashField) + } + case AppHashField: + if bytes.Equal(committedHeader.AppHash, e.Header.AppHash) { + return matchErr(AppHashField) + } + case LastResultsHashField: + if bytes.Equal(committedHeader.LastResultsHash, e.Header.LastResultsHash) { + return matchErr(LastResultsHashField) + } + default: + return errors.New("unknown InvalidHeaderField") + } + + return nil +} + +//------------------------------------------- + +type PotentialAmnesiaEvidence struct { + VoteA *Vote `json:"vote_a"` + VoteB *Vote `json:"vote_b"` +} + +var _ Evidence = &PotentialAmnesiaEvidence{} +var _ Evidence = PotentialAmnesiaEvidence{} + +func (e PotentialAmnesiaEvidence) Height() int64 { + return e.VoteA.Height +} + +func (e PotentialAmnesiaEvidence) Time() time.Time { + if e.VoteA.Timestamp.Before(e.VoteB.Timestamp) { + return e.VoteA.Timestamp + } + return e.VoteB.Timestamp +} + +func (e PotentialAmnesiaEvidence) Address() []byte { + return e.VoteA.ValidatorAddress +} + +func (e PotentialAmnesiaEvidence) Hash() []byte { + return tmhash.Sum(cdcEncode(e)) +} + +func (e PotentialAmnesiaEvidence) Bytes() []byte { + return cdcEncode(e) +} + +func (e PotentialAmnesiaEvidence) Verify(chainID string, pubKey crypto.PubKey) error { + // 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()) + } + + // Signatures must be valid + if !pubKey.VerifyBytes(e.VoteA.SignBytes(chainID), e.VoteA.Signature) { + return fmt.Errorf("verifying VoteA: %w", ErrVoteInvalidSignature) + } + if !pubKey.VerifyBytes(e.VoteB.SignBytes(chainID), e.VoteB.Signature) { + return fmt.Errorf("verifying VoteB: %w", ErrVoteInvalidSignature) + } + + return nil +} + +func (e PotentialAmnesiaEvidence) Equal(ev Evidence) bool { + switch e2 := ev.(type) { + case PotentialAmnesiaEvidence: + return bytes.Equal(e.Hash(), e2.Hash()) + case *PotentialAmnesiaEvidence: + return bytes.Equal(e.Hash(), e2.Hash()) + default: + return false + } +} + +func (e PotentialAmnesiaEvidence) ValidateBasic() error { + if e.VoteA == nil || e.VoteB == nil { + return fmt.Errorf("one or both of the votes are empty %v, %v", e.VoteA, e.VoteB) + } + if err := e.VoteA.ValidateBasic(); err != nil { + return fmt.Errorf("invalid VoteA: %v", err) + } + if err := e.VoteB.ValidateBasic(); err != nil { + return fmt.Errorf("invalid VoteB: %v", err) + } + // Enforce Votes are lexicographically sorted on blockID + if strings.Compare(e.VoteA.BlockID.Key(), e.VoteB.BlockID.Key()) >= 0 { + return errors.New("amnesia votes in invalid order") + } + + // H/S must be the same + if e.VoteA.Height != e.VoteB.Height || + e.VoteA.Type != e.VoteB.Type { + return fmt.Errorf("h/s do not match: %d/%v vs %d/%v", + e.VoteA.Height, e.VoteA.Type, e.VoteB.Height, e.VoteB.Type) + } + + // R must be different + if e.VoteA.Round == e.VoteB.Round { + return fmt.Errorf("expected votes from different rounds, got %d", e.VoteA.Round) + } + + // 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, + ) + } + + // Index must be the same + // https://github.com/tendermint/tendermint/issues/4619 + if e.VoteA.ValidatorIndex != e.VoteB.ValidatorIndex { + return fmt.Errorf( + "duplicateVoteEvidence Error: Validator indices do not match. Got %d and %d", + e.VoteA.ValidatorIndex, + e.VoteB.ValidatorIndex, + ) + } + + // 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, + ) + } + + return nil +} + +func (e PotentialAmnesiaEvidence) String() string { + return fmt.Sprintf("PotentialAmnesiaEvidence{VoteA: %v, VoteB: %v}", e.VoteA, e.VoteB) +} + //----------------------------------------------------------------- // UNSTABLE @@ -307,38 +985,3 @@ func (e MockEvidence) ValidateBasic() error { return nil } func (e MockEvidence) String() string { return fmt.Sprintf("Evidence: %d/%s/%s", e.EvidenceHeight, e.Time(), e.EvidenceAddress) } - -//------------------------------------------- - -// EvidenceList is a list of Evidence. Evidences is not a word. -type EvidenceList []Evidence - -// Hash returns the simple merkle root hash of the EvidenceList. -func (evl EvidenceList) Hash() []byte { - // These allocations are required because Evidence is not of type Bytes, and - // golang slices can't be typed cast. This shouldn't be a performance problem since - // the Evidence size is capped. - evidenceBzs := make([][]byte, len(evl)) - for i := 0; i < len(evl); i++ { - evidenceBzs[i] = evl[i].Bytes() - } - return merkle.SimpleHashFromByteSlices(evidenceBzs) -} - -func (evl EvidenceList) String() string { - s := "" - for _, e := range evl { - s += fmt.Sprintf("%s\t\t", e) - } - return s -} - -// Has returns true if the evidence is in the EvidenceList. -func (evl EvidenceList) Has(evidence Evidence) bool { - for _, ev := range evl { - if ev.Equal(evidence) { - return true - } - } - return false -} diff --git a/types/evidence_test.go b/types/evidence_test.go index 40e096fcd..f9277bb7b 100644 --- a/types/evidence_test.go +++ b/types/evidence_test.go @@ -8,8 +8,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/crypto/secp256k1" "github.com/tendermint/tendermint/crypto/tmhash" + tmrand "github.com/tendermint/tendermint/libs/rand" ) type voteData struct { @@ -176,3 +179,195 @@ func TestMockBadEvidenceValidateBasic(t *testing.T) { badEvidence := NewMockEvidence(int64(1), time.Now(), 1, []byte{1}) assert.Nil(t, badEvidence.ValidateBasic()) } + +func TestLunaticValidatorEvidence(t *testing.T) { + var ( + blockID = makeBlockIDRandom() + header = makeHeaderRandom() + bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") + val = NewMockPV() + vote = makeVote(t, val, header.ChainID, 0, header.Height, 0, 2, blockID) + ) + + header.Time = bTime + + ev := &LunaticValidatorEvidence{ + Header: header, + Vote: vote, + InvalidHeaderField: "AppHash", + } + + assert.Equal(t, header.Height, ev.Height()) + assert.Equal(t, bTime, ev.Time()) + assert.EqualValues(t, vote.ValidatorAddress, ev.Address()) + assert.NotEmpty(t, ev.Hash()) + assert.NotEmpty(t, ev.Bytes()) + pubKey, err := val.GetPubKey() + require.NoError(t, err) + assert.NoError(t, ev.Verify(header.ChainID, pubKey)) + assert.Error(t, ev.Verify("other", pubKey)) + privKey2 := ed25519.GenPrivKey() + pubKey2 := privKey2.PubKey() + assert.Error(t, ev.Verify("other", pubKey2)) + assert.True(t, ev.Equal(ev)) + assert.NoError(t, ev.ValidateBasic()) + assert.NotEmpty(t, ev.String()) +} + +func TestPhantomValidatorEvidence(t *testing.T) { + var ( + blockID = makeBlockIDRandom() + header = makeHeaderRandom() + bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") + val = NewMockPV() + vote = makeVote(t, val, header.ChainID, 0, header.Height, 0, 2, blockID) + ) + + header.Time = bTime + + ev := &PhantomValidatorEvidence{ + Header: header, + Vote: vote, + LastHeightValidatorWasInSet: header.Height - 1, + } + + assert.Equal(t, header.Height, ev.Height()) + assert.Equal(t, bTime, ev.Time()) + assert.EqualValues(t, vote.ValidatorAddress, ev.Address()) + assert.NotEmpty(t, ev.Hash()) + assert.NotEmpty(t, ev.Bytes()) + pubKey, err := val.GetPubKey() + require.NoError(t, err) + assert.NoError(t, ev.Verify(header.ChainID, pubKey)) + assert.Error(t, ev.Verify("other", pubKey)) + privKey2 := ed25519.GenPrivKey() + pubKey2 := privKey2.PubKey() + assert.Error(t, ev.Verify("other", pubKey2)) + assert.True(t, ev.Equal(ev)) + assert.NoError(t, ev.ValidateBasic()) + assert.NotEmpty(t, ev.String()) +} + +func TestConflictingHeadersEvidence(t *testing.T) { + const ( + chainID = "TestConflictingHeadersEvidence" + height int64 = 37 + ) + + var ( + blockID = makeBlockIDRandom() + header1 = makeHeaderRandom() + header2 = makeHeaderRandom() + ) + + header1.Height = height + header1.LastBlockID = blockID + header1.ChainID = chainID + + header2.Height = height + header2.LastBlockID = blockID + header2.ChainID = chainID + + voteSet1, valSet, vals := randVoteSet(height, 1, PrecommitType, 10, 1) + voteSet2 := NewVoteSet(chainID, height, 1, PrecommitType, valSet) + + commit1, err := MakeCommit(BlockID{ + Hash: header1.Hash(), + PartsHeader: PartSetHeader{ + Total: 100, + Hash: crypto.CRandBytes(tmhash.Size), + }, + }, height, 1, voteSet1, vals, time.Now()) + require.NoError(t, err) + commit2, err := MakeCommit(BlockID{ + Hash: header2.Hash(), + PartsHeader: PartSetHeader{ + Total: 100, + Hash: crypto.CRandBytes(tmhash.Size), + }, + }, height, 1, voteSet2, vals, time.Now()) + require.NoError(t, err) + + ev := &ConflictingHeadersEvidence{ + H1: &SignedHeader{ + Header: header1, + Commit: commit1, + }, + H2: &SignedHeader{ + Header: header2, + Commit: commit2, + }, + } + + assert.Panics(t, func() { + ev.Address() + }) + + assert.Panics(t, func() { + pubKey, _ := vals[0].GetPubKey() + ev.Verify(chainID, pubKey) + }) + + assert.Equal(t, height, ev.Height()) + // assert.Equal(t, bTime, ev.Time()) + assert.NotEmpty(t, ev.Hash()) + assert.NotEmpty(t, ev.Bytes()) + assert.NoError(t, ev.VerifyComposite(header1, valSet)) + assert.True(t, ev.Equal(ev)) + assert.NoError(t, ev.ValidateBasic()) + assert.NotEmpty(t, ev.String()) +} + +func TestPotentialAmnesiaEvidence(t *testing.T) { + const ( + chainID = "TestPotentialAmnesiaEvidence" + height int64 = 37 + ) + + var ( + val = NewMockPV() + blockID = makeBlockID(tmhash.Sum([]byte("blockhash")), math.MaxInt64, tmhash.Sum([]byte("partshash"))) + blockID2 = makeBlockID(tmhash.Sum([]byte("blockhash2")), math.MaxInt64, tmhash.Sum([]byte("partshash"))) + vote1 = makeVote(t, val, chainID, 0, height, 0, 2, blockID) + vote2 = makeVote(t, val, chainID, 0, height, 1, 2, blockID2) + ) + + ev := &PotentialAmnesiaEvidence{ + VoteA: vote2, + VoteB: vote1, + } + + assert.Equal(t, height, ev.Height()) + // assert.Equal(t, bTime, ev.Time()) + assert.EqualValues(t, vote1.ValidatorAddress, ev.Address()) + assert.NotEmpty(t, ev.Hash()) + assert.NotEmpty(t, ev.Bytes()) + pubKey, err := val.GetPubKey() + require.NoError(t, err) + assert.NoError(t, ev.Verify(chainID, pubKey)) + assert.Error(t, ev.Verify("other", pubKey)) + privKey2 := ed25519.GenPrivKey() + pubKey2 := privKey2.PubKey() + assert.Error(t, ev.Verify("other", pubKey2)) + assert.True(t, ev.Equal(ev)) + assert.NoError(t, ev.ValidateBasic()) + assert.NotEmpty(t, ev.String()) +} + +func makeHeaderRandom() *Header { + return &Header{ + ChainID: tmrand.Str(12), + Height: int64(tmrand.Uint16()) + 1, + Time: time.Now(), + LastBlockID: makeBlockIDRandom(), + 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(tmhash.Size), + } +} diff --git a/types/protobuf.go b/types/protobuf.go index 52815593f..a185722f1 100644 --- a/types/protobuf.go +++ b/types/protobuf.go @@ -150,7 +150,8 @@ func (tm2pb) ConsensusParams(params *ConsensusParams) *abci.ConsensusParams { // so Evidence types stays compact. // XXX: panics on nil or unknown pubkey type func (tm2pb) Evidence(ev Evidence, valSet *ValidatorSet, evTime time.Time) abci.Evidence { - _, val := valSet.GetByAddress(ev.Address()) + addr := ev.Address() + _, val := valSet.GetByAddress(addr) if val == nil { // should already have checked this panic(val) @@ -161,6 +162,12 @@ func (tm2pb) Evidence(ev Evidence, valSet *ValidatorSet, evTime time.Time) abci. switch ev.(type) { case *DuplicateVoteEvidence: evType = ABCIEvidenceTypeDuplicateVote + case *PhantomValidatorEvidence: + evType = "phantom" + case *LunaticValidatorEvidence: + evType = "lunatic" + case *PotentialAmnesiaEvidence: + evType = "potential_amnesia" case MockEvidence: // XXX: not great to have test types in production paths ... evType = ABCIEvidenceTypeMock diff --git a/types/vote.go b/types/vote.go index da9134cd6..37520fec3 100644 --- a/types/vote.go +++ b/types/vote.go @@ -31,7 +31,7 @@ type ErrVoteConflictingVotes struct { } func (err *ErrVoteConflictingVotes) Error() string { - return fmt.Sprintf("Conflicting votes from validator %v", err.PubKey.Address()) + return fmt.Sprintf("conflicting votes from validator %X", err.PubKey.Address()) } func NewConflictingVoteError(val *Validator, vote1, vote2 *Vote) *ErrVoteConflictingVotes {