Browse Source

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
pull/4716/head
Anton Kaliaev 4 years ago
committed by GitHub
parent
commit
41c11ad2c1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1831 additions and 474 deletions
  1. +2
    -0
      CHANGELOG_PENDING.md
  2. +6
    -6
      abci/types/types.proto
  3. +38
    -6
      docs/architecture/adr-047-handling-evidence-from-light-client.md
  4. +186
    -36
      evidence/pool.go
  5. +160
    -59
      evidence/pool_test.go
  6. +21
    -12
      evidence/reactor_test.go
  7. +21
    -28
      evidence/store_test.go
  8. +21
    -4
      lite2/client.go
  9. +40
    -0
      lite2/client_test.go
  10. +18
    -1
      lite2/errors.go
  11. +21
    -23
      lite2/provider/http/http.go
  12. +7
    -2
      lite2/provider/http/http_test.go
  13. +9
    -8
      lite2/provider/mock/deadmock.go
  14. +28
    -14
      lite2/provider/mock/mock.go
  15. +3
    -0
      lite2/provider/provider.go
  16. +1
    -2
      lite2/verifier_test.go
  17. +6
    -3
      node/node.go
  18. +5
    -2
      node/node_test.go
  19. +210
    -0
      rpc/client/evidence_test.go
  20. +45
    -26
      rpc/client/http/http.go
  21. +8
    -0
      rpc/client/interface.go
  22. +2
    -153
      rpc/client/rpc_test.go
  23. +3
    -2
      rpc/core/blocks.go
  24. +6
    -0
      rpc/core/evidence.go
  25. +4
    -1
      state/store.go
  26. +44
    -12
      state/validation.go
  27. +20
    -23
      types/block.go
  28. +692
    -49
      types/evidence.go
  29. +195
    -0
      types/evidence_test.go
  30. +8
    -1
      types/protobuf.go
  31. +1
    -1
      types/vote.go

+ 2
- 0
CHANGELOG_PENDING.md View File

@ -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:


+ 6
- 6
abci/types/types.proto View File

@ -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"];
}
//----------------------------------------


+ 38
- 6
docs/architecture/adr-047-handling-evidence-from-light-client.md View File

@ -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.


+ 186
- 36
evidence/pool.go View File

@ -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{}
valSet, err := sm.LoadValidators(evpool.stateDB, evidence.Height())
if err != nil {
return fmt.Errorf("can't load validators at height #%d: %w", evidence.Height(), err)
}
if err := sm.VerifyEvidence(evpool.stateDB, evpool.State(), evidence); err != nil {
return ErrInvalidEvidence{err}
}
// Break composite evidence into smaller pieces.
if ce, ok := evidence.(types.CompositeEvidence); ok {
evpool.logger.Info("Breaking up composite evidence", "ev", evidence)
// fetch the validator and return its voting power as its priority
// TODO: something better ?
valset, err := sm.LoadValidators(evpool.stateDB, evidence.Height())
if err != nil {
return err
}
_, val := valset.GetByAddress(evidence.Address())
priority := val.VotingPower
blockMeta := evpool.blockStore.LoadBlockMeta(evidence.Height())
if blockMeta == nil {
return fmt.Errorf("don't have block meta at height #%d", evidence.Height())
}
_, err = evpool.store.AddNewEvidence(evidence, priority)
if err != nil {
return err
if err := ce.VerifyComposite(&blockMeta.Header, valSet); err != nil {
return err
}
evList = ce.Split(&blockMeta.Header, valSet, evpool.valToLastHeight)
}
evpool.logger.Info("Verified new evidence of byzantine behaviour", "evidence", evidence)
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)
// add evidence to clist
evpool.evidenceList.PushBack(evidence)
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
}

+ 160
- 59
evidence/pool_test.go View File

@ -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)
}

+ 21
- 12
evidence/reactor_test.go View File

@ -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()


+ 21
- 28
evidence/store_test.go View File

@ -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)
}
}

+ 21
- 4
lite2/client.go View File

@ -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 {


+ 40
- 0
lite2/client_test.go View File

@ -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))
}

+ 18
- 1
lite2/errors.go View File

@ -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"
}

+ 21
- 23
lite2/provider/http/http.go View File

@ -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)


+ 7
- 2
lite2/provider/http/http_test.go View File

@ -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


+ 9
- 8
lite2/provider/mock/deadmock.go View File

@ -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
}

+ 28
- 14
lite2/provider/mock/mock.go View File

@ -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
}

+ 3
- 0
lite2/provider/provider.go View File

@ -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
}

+ 1
- 2
lite2/verifier_test.go View File

@ -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: {


+ 6
- 3
node/node.go View File

@ -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
}


+ 5
- 2
node/node_test.go View File

@ -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


+ 210
- 0
rpc/client/evidence_test.go View File

@ -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")
}
}

+ 45
- 26
rpc/client/http/http.go View File

@ -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
}


+ 8
- 0
rpc/client/interface.go View File

@ -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
}

+ 2
- 153
rpc/client/rpc_test.go View File

@ -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)


+ 3
- 2
rpc/core/blocks.go View File

@ -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",


+ 6
- 0
rpc/core/evidence.go View File

@ -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


+ 4
- 1
state/store.go View File

@ -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)
}
}
//------------------------------------------------------------------------


+ 44
- 12
state/validation.go View File

@ -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 {


+ 20
- 23
types/block.go View File

@ -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)")
}
// 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)
}
// Check ChainID.
// 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)
}
// 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")
return fmt.Errorf("header and commit height mismatch: %d vs %d", sh.Height, sh.Commit.Height)
}
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
}


+ 692
- 49
types/evidence.go View File

@ -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
}

+ 195
- 0
types/evidence_test.go View File

@ -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),
}
}

+ 8
- 1
types/protobuf.go View File

@ -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


+ 1
- 1
types/vote.go View File

@ -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 {


Loading…
Cancel
Save