Browse Source

evidence: improve amnesia evidence handling (#5003)

fix bug so that PotentialAmnesiaEvidence is being gossiped

handle inbound amnesia evidence correctly

add method to check if potential amnesia evidence is on trial

fix a bug with the height when we upgrade to amnesia evidence

change evidence to using just pointers.

More logging in the evidence module

Co-authored-by: Marko <marbar3778@yahoo.com>
pull/5040/head
Callum Waters 4 years ago
committed by GitHub
parent
commit
65d7ce9c9c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 827 additions and 538 deletions
  1. +2
    -2
      consensus/reactor_test.go
  2. +1
    -1
      consensus/replay_stubs.go
  3. +2
    -2
      consensus/state.go
  4. +4
    -4
      consensus/state_test.go
  5. +27
    -3
      evidence/doc.go
  6. +167
    -91
      evidence/pool.go
  7. +132
    -34
      evidence/pool_test.go
  8. +2
    -2
      light/client.go
  9. +1
    -1
      light/client_test.go
  10. +1
    -1
      rpc/client/evidence_test.go
  11. +7
    -6
      state/validation.go
  12. +76
    -28
      state/validation_test.go
  13. +2
    -2
      types/block.go
  14. +287
    -267
      types/evidence.go
  15. +116
    -94
      types/evidence_test.go

+ 2
- 2
consensus/reactor_test.go View File

@ -234,8 +234,8 @@ func (m *mockEvidencePool) IsPending(evidence types.Evidence) bool {
}
return false
}
func (m *mockEvidencePool) AddPOLC(types.ProofOfLockChange) error { return nil }
func (m *mockEvidencePool) Header(int64) *types.Header { return nil }
func (m *mockEvidencePool) AddPOLC(*types.ProofOfLockChange) error { return nil }
func (m *mockEvidencePool) Header(int64) *types.Header { return nil }
//------------------------------------


+ 1
- 1
consensus/replay_stubs.go View File

@ -56,7 +56,7 @@ func (emptyEvidencePool) AddEvidence(types.Evidence) error { return nil }
func (emptyEvidencePool) Update(*types.Block, sm.State) {}
func (emptyEvidencePool) IsCommitted(types.Evidence) bool { return false }
func (emptyEvidencePool) IsPending(types.Evidence) bool { return false }
func (emptyEvidencePool) AddPOLC(types.ProofOfLockChange) error { return nil }
func (emptyEvidencePool) AddPOLC(*types.ProofOfLockChange) error { return nil }
func (emptyEvidencePool) Header(int64) *types.Header { return nil }
//-----------------------------------------------------------------------------


+ 2
- 2
consensus/state.go View File

@ -70,7 +70,7 @@ type txNotifier interface {
// interface to the evidence pool
type evidencePool interface {
AddEvidence(types.Evidence) error
AddPOLC(types.ProofOfLockChange) error
AddPOLC(*types.ProofOfLockChange) error
}
// State handles execution of the consensus algorithm.
@ -1312,7 +1312,7 @@ func (cs *State) savePOLC(round int32, blockID types.BlockID) {
cs.Logger.Error("Error on retrieval of pubkey", "err", err)
return
}
polc, err := types.MakePOLCFromVoteSet(cs.Votes.Prevotes(round), pubKey, blockID)
polc, err := types.NewPOLCFromVoteSet(cs.Votes.Prevotes(round), pubKey, blockID)
if err != nil {
cs.Logger.Error("Error on forming POLC", "err", err)
return


+ 4
- 4
consensus/state_test.go View File

@ -710,13 +710,12 @@ func TestStateLockPOLUnlock(t *testing.T) {
// polc should be in the evpool for round 1
polc, err := evpool.RetrievePOLC(height, round)
assert.NoError(t, err)
assert.NotNil(t, polc)
assert.False(t, polc.IsAbsent())
t.Log(polc.Address())
// but not for round 0
polc, err = evpool.RetrievePOLC(height, round-1)
assert.Error(t, err)
assert.True(t, polc.IsAbsent())
assert.NoError(t, err)
assert.Nil(t, polc)
}
// 4 vals, v1 locks on proposed block in the first round but the other validators only prevote
@ -820,6 +819,7 @@ func TestStateLockPOLUnlockOnUnknownBlock(t *testing.T) {
// polc should be in the evpool for round 1
polc, err := evpool.RetrievePOLC(height, round)
assert.NoError(t, err)
assert.NotNil(t, polc)
assert.False(t, polc.IsAbsent())
incrementRound(vs2, vs3, vs4)


+ 27
- 3
evidence/doc.go View File

@ -24,15 +24,39 @@ uncommitted evidence at intervals of 60 seconds (set by the by broadcastEvidence
It uses a concurrent list to store the evidence and before sending verifies that each evidence is still valid in the
sense that it has not exceeded the max evidence age and height (see types/params.go#EvidenceParams).
Three are four buckets that evidence can be stored in: Pending, Committed, Awaiting and POLC's.
1. Pending is awaiting to be committed (evidence is usually broadcasted then)
2. Committed is for those already on the block and is to ensure that evidence isn't submitted twice
3. AwaitingTrial primarily refers to PotentialAmnesiaEvidence which must wait for a trial period before
being ready to be submitted (see docs/architecture/adr-056)
4. POLC's store all the ProofOfLockChanges that the node has done as part of consensus. To change lock is to vote
for a different block in a later round. The consensus module calls `AddPOLC()` to add to this bucket.
All evidence is proto encoded to disk.
Proposing
When a new block is being proposed (in state/execution.go#CreateProposalBlock),
`PendingEvidence(maxNum)` is called to send up to the maxNum number of uncommitted evidence, from the evidence store,
based on a priority that is a product of the age of the evidence and the voting power of the malicious validator.
prioritized in order of age. All evidence is checked for expiration.
When a node receives evidence in a block it will use the evidence module as a cache first to see if it has
already verified the evidence before trying to verify it again.
Once the proposed evidence is submitted,
the evidence is marked as committed and is moved from the broadcasted set to the committed set (
the committed set is used to verify whether new evidence has actually already been submitted).
the evidence is marked as committed and is moved from the broadcasted set to the committed set.
As a result it is also removed from the concurrent list so that it is no longer gossiped.
Minor Functionality
As all evidence (including POLC's) are bounded by an expiration date, those that exceed this are no longer needed
and hence pruned. Currently, only committed evidence in which a marker to the height that the evidence was committed
and hence very small is saved. All updates are made from the `Update(block, state)` function which should be called
when a new block is committed.
*/
package evidence

+ 167
- 91
evidence/pool.go View File

@ -18,10 +18,10 @@ import (
)
const (
baseKeyCommitted = byte(0x00)
baseKeyPending = byte(0x01)
baseKeyPOLC = byte(0x02)
baseKeyAwaiting = byte(0x03)
baseKeyCommitted = byte(0x00)
baseKeyPending = byte(0x01)
baseKeyPOLC = byte(0x02)
baseKeyAwaitingTrial = byte(0x03)
)
// Pool maintains a pool of valid evidence to be broadcasted and committed
@ -51,6 +51,8 @@ type Pool struct {
// Validator.Address -> Last height it was in validator set
type valToLastHeightMap map[string]int64
// Creates a new pool. If using an existing evidence store, it will add all pending evidence
// to the concurrent list.
func NewPool(stateDB, evidenceDB dbm.DB, blockStore *store.BlockStore) (*Pool, error) {
var (
state = sm.LoadState(stateDB)
@ -82,7 +84,7 @@ func NewPool(stateDB, evidenceDB dbm.DB, blockStore *store.BlockStore) (*Pool, e
}
// PendingEvidence is used primarily as part of block proposal and returns up to maxNum of uncommitted evidence.
// If maxNum is -1, all evidence is returned. Pending evidence is prioritised based on time.
// If maxNum is -1, all evidence is returned. Pending evidence is prioritized based on time.
func (evpool *Pool) PendingEvidence(maxNum uint32) []types.Evidence {
evpool.removeExpiredPendingEvidence()
evidence, err := evpool.listEvidence(baseKeyPending, int64(maxNum))
@ -92,6 +94,7 @@ func (evpool *Pool) PendingEvidence(maxNum uint32) []types.Evidence {
return evidence
}
// AllPendingEvidence returns all evidence ready to be proposed and committed.
func (evpool *Pool) AllPendingEvidence() []types.Evidence {
evpool.removeExpiredPendingEvidence()
evidence, err := evpool.listEvidence(baseKeyPending, -1)
@ -113,29 +116,30 @@ func (evpool *Pool) Update(block *types.Block, state sm.State) {
)
}
// update the state
evpool.updateState(state)
// remove evidence from pending and mark committed
evpool.MarkEvidenceAsCommitted(block.Height, block.Evidence.Evidence)
// prune pending, committed and potential evidence and polc's periodically
if block.Height%state.ConsensusParams.Evidence.MaxAgeNumBlocks == 0 {
evpool.logger.Debug("Pruning no longer necessary evidence")
evpool.pruneExpiredPOLC()
evpool.removeExpiredPendingEvidence()
}
if evpool.nextEvidenceTrialEndedHeight > 0 && block.Height < evpool.nextEvidenceTrialEndedHeight {
evpool.upgradePotentialAmnesiaEvidence()
}
// update the state
evpool.mtx.Lock()
defer evpool.mtx.Unlock()
evpool.state = state
evpool.updateValToLastHeight(block.Height, state)
if evpool.nextEvidenceTrialEndedHeight > 0 && block.Height > evpool.nextEvidenceTrialEndedHeight {
evpool.logger.Debug("Upgrading all potential evidence that have served the trial period")
evpool.nextEvidenceTrialEndedHeight = evpool.upgradePotentialAmnesiaEvidence()
}
}
// AddPOLC adds a proof of lock change to the evidence database
// that may be needed in the future to verify votes
func (evpool *Pool) AddPOLC(polc types.ProofOfLockChange) error {
func (evpool *Pool) AddPOLC(polc *types.ProofOfLockChange) error {
key := keyPOLC(polc)
pbplc, err := polc.ToProto()
if err != nil {
@ -157,6 +161,8 @@ func (evpool *Pool) AddEvidence(evidence types.Evidence) error {
evList = []types.Evidence{evidence}
)
evpool.logger.Debug("Attempting to add evidence", "ev", evidence)
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)
@ -187,8 +193,13 @@ func (evpool *Pool) AddEvidence(evidence types.Evidence) error {
}
for _, ev := range evList {
if evpool.Has(ev) {
continue
// if it is an amnesia evidence we have but POLC is not absent then
// we should still process it
if ae, ok := ev.(*types.AmnesiaEvidence); !ok || ae.Polc.IsAbsent() {
continue
}
}
// For lunatic validator evidence, a header needs to be fetched.
@ -206,68 +217,32 @@ func (evpool *Pool) AddEvidence(evidence types.Evidence) error {
}
// For potential amnesia evidence, if this node is indicted it shall retrieve a polc
// to form AmensiaEvidence
if pe, ok := ev.(types.PotentialAmnesiaEvidence); ok {
var (
height = pe.Height()
exists = false
polc types.ProofOfLockChange
)
pe.HeightStamp = evpool.State().LastBlockHeight
// a) first try to find a corresponding polc
for round := pe.VoteB.Round; round > pe.VoteA.Round; round-- {
polc, err = evpool.RetrievePOLC(height, round)
if err != nil {
evpool.logger.Error("Failed to retrieve polc for potential amnesia evidence", "err", err, "pae", pe.String())
continue
}
if err == nil && !polc.IsAbsent() {
// we should not need to verify it if both the polc and potential amnesia evidence have already
// been verified. We replace the potential amnesia evidence.
ae := types.MakeAmnesiaEvidence(pe, polc)
err := evpool.AddEvidence(ae)
if err != nil {
evpool.logger.Error("Failed to create amnesia evidence from potential amnesia evidence", "err", err)
// revert back to processing potential amnesia evidence
exists = false
} else {
evpool.logger.Info("Formed amnesia evidence from own polc", "amnesiaEvidence", ae)
}
break
}
// to form AmensiaEvidence else start the trial period for the piece of evidence
if pe, ok := ev.(*types.PotentialAmnesiaEvidence); ok {
if err := evpool.handleInboundPotentialAmnesiaEvidence(pe); err != nil {
return err
}
// b) check if amnesia evidence can be made now or if we need to enact the trial period
if !exists && pe.Primed(1, pe.HeightStamp) {
err := evpool.AddEvidence(types.MakeAmnesiaEvidence(pe, types.EmptyPOLC()))
if err != nil {
return err
}
} else if !exists && evpool.State().LastBlockHeight+evpool.State().ConsensusParams.Evidence.ProofTrialPeriod <
pe.Height()+evpool.State().ConsensusParams.Evidence.MaxAgeNumBlocks {
// if we can't find a proof of lock change and we know that the trial period will finish before the
// evidence has expired, then we commence the trial period by saving it in the awaiting bucket
pbe, err := types.EvidenceToProto(pe)
if err != nil {
return err
}
evBytes, err := pbe.Marshal()
if err != nil {
return err
}
key := keyAwaiting(pe)
err = evpool.evidenceStore.Set(key, evBytes)
if err != nil {
return err
continue
} else if ae, ok := ev.(*types.AmnesiaEvidence); ok {
if ae.Polc.IsAbsent() && ae.PotentialAmnesiaEvidence.VoteA.Round <
ae.PotentialAmnesiaEvidence.VoteB.Round {
if err := evpool.handleInboundPotentialAmnesiaEvidence(ae.PotentialAmnesiaEvidence); err != nil {
return fmt.Errorf("failed to handle amnesia evidence, err: %w", err)
}
// keep track of when the next pe has finished the trial period
if evpool.nextEvidenceTrialEndedHeight == -1 {
evpool.nextEvidenceTrialEndedHeight = ev.Height() + evpool.State().ConsensusParams.Evidence.ProofTrialPeriod
continue
} else {
// we are going to add this amnesia evidence and check if we already have an amnesia evidence or potential
// amnesia evidence that addesses the same case
aeWithoutPolc := types.NewAmnesiaEvidence(ae.PotentialAmnesiaEvidence, types.NewEmptyPOLC())
if evpool.IsPending(aeWithoutPolc) {
evpool.removePendingEvidence(aeWithoutPolc)
} else if evpool.IsOnTrial(ae.PotentialAmnesiaEvidence) {
key := keyAwaitingTrial(ae.PotentialAmnesiaEvidence)
if err := evpool.evidenceStore.Delete(key); err != nil {
evpool.logger.Error("Failed to remove potential amnesia evidence from database", "err", err)
}
}
}
// we don't need to do anymore processing so we can move on to the next piece of evidence
continue
}
// 2) Save to store.
@ -319,7 +294,7 @@ func (evpool *Pool) MarkEvidenceAsCommitted(height int64, evidence []types.Evide
// Has checks whether the evidence exists either pending or already committed
func (evpool *Pool) Has(evidence types.Evidence) bool {
return evpool.IsPending(evidence) || evpool.IsCommitted(evidence)
return evpool.IsPending(evidence) || evpool.IsCommitted(evidence) || evpool.IsOnTrial(evidence)
}
// IsEvidenceExpired checks whether evidence is past the maximum age where it can be used
@ -359,31 +334,49 @@ func (evpool *Pool) IsPending(evidence types.Evidence) bool {
return ok
}
// IsOnTrial checks whether a piece of evidence is in the awaiting bucket.
// Only Potential Amnesia Evidence is stored here.
func (evpool *Pool) IsOnTrial(evidence types.Evidence) bool {
pe, ok := evidence.(*types.PotentialAmnesiaEvidence)
if !ok {
return false
}
key := keyAwaitingTrial(pe)
ok, err := evpool.evidenceStore.Has(key)
if err != nil {
evpool.logger.Error("Unable to find evidence on trial", "err", err)
}
return ok
}
// RetrievePOLC attempts to find a polc at the given height and round, if not there than exist returns false, all
// database errors are automatically logged
func (evpool *Pool) RetrievePOLC(height int64, round int32) (polc types.ProofOfLockChange, err error) {
func (evpool *Pool) RetrievePOLC(height int64, round int32) (*types.ProofOfLockChange, error) {
var pbpolc tmproto.ProofOfLockChange
key := keyPOLCFromHeightAndRound(height, round)
polcBytes, err := evpool.evidenceStore.Get(key)
if err != nil {
evpool.logger.Error("Unable to retrieve polc", "err", err)
return polc, err
return nil, err
}
// polc doesn't exist
if polcBytes == nil {
return polc, fmt.Errorf("nil value in database for key: %s", key)
return nil, nil
}
err = proto.Unmarshal(polcBytes, &pbpolc)
if err != nil {
return polc, err
return nil, err
}
plc, err := types.ProofOfLockChangeFromProto(&pbpolc)
polc, err := types.ProofOfLockChangeFromProto(&pbpolc)
if err != nil {
return polc, err
return nil, err
}
return *plc, err
return polc, err
}
// EvidenceFront goes to the first evidence in the clist
@ -580,16 +573,23 @@ func (evpool *Pool) pruneExpiredPOLC() {
}
}
func (evpool *Pool) updateState(state sm.State) {
evpool.mtx.Lock()
defer evpool.mtx.Unlock()
evpool.state = state
}
// upgrades any potential evidence that has undergone the trial period and is primed to be made into
// amnesia evidence
func (evpool *Pool) upgradePotentialAmnesiaEvidence() int64 {
iter, err := dbm.IteratePrefix(evpool.evidenceStore, []byte{baseKeyAwaiting})
iter, err := dbm.IteratePrefix(evpool.evidenceStore, []byte{baseKeyAwaitingTrial})
if err != nil {
evpool.logger.Error("Unable to iterate over POLC's", "err", err)
return -1
}
defer iter.Close()
trialPeriod := evpool.State().ConsensusParams.Evidence.ProofTrialPeriod
currentHeight := evpool.State().LastBlockHeight
// 1) Iterate through all potential amnesia evidence in order of height
for ; iter.Valid(); iter.Next() {
paeBytes := iter.Value()
@ -602,26 +602,27 @@ func (evpool *Pool) upgradePotentialAmnesiaEvidence() int64 {
}
ev, err := types.EvidenceFromProto(&evpb)
if err != nil {
evpool.logger.Error("coverting to evidence from proto", "err", err)
evpool.logger.Error("Converting from proto to evidence", "err", err)
continue
}
// 3) Check if the trial period has lapsed and amnesia evidence can be formed
if pe, ok := ev.(*types.PotentialAmnesiaEvidence); ok {
if pe.Primed(trialPeriod, evpool.State().LastBlockHeight) {
ae := types.MakeAmnesiaEvidence(*pe, types.EmptyPOLC())
err := evpool.AddEvidence(ae)
if pe.Primed(trialPeriod, currentHeight) {
ae := types.NewAmnesiaEvidence(pe, types.NewEmptyPOLC())
err := evpool.addPendingEvidence(ae)
if err != nil {
evpool.logger.Error("Unable to add amnesia evidence", "err", err)
continue
}
evpool.logger.Info("Upgraded to amnesia evidence", "amnesiaEvidence", ae)
err = evpool.evidenceStore.Delete(iter.Key())
if err != nil {
evpool.logger.Error("Unable to delete potential amnesia evidence", "err", err)
continue
}
} else {
evpool.logger.Debug("Potential amnesia evidence not ready to be upgraded. Ready at height", "height",
pe.HeightStamp+trialPeriod)
evpool.logger.Debug("Potential amnesia evidence is not ready to be upgraded. Ready at", "height",
pe.HeightStamp+trialPeriod, "currentHeight", currentHeight)
// once we reach a piece of evidence that isn't ready send back the height with which it will be ready
return pe.HeightStamp + trialPeriod
}
@ -631,6 +632,81 @@ func (evpool *Pool) upgradePotentialAmnesiaEvidence() int64 {
return -1
}
func (evpool *Pool) handleInboundPotentialAmnesiaEvidence(pe *types.PotentialAmnesiaEvidence) error {
var (
height = pe.Height()
exists = false
polc *types.ProofOfLockChange
err error
)
evpool.logger.Debug("Received Potential Amnesia Evidence", "pe", pe)
// a) first try to find a corresponding polc
for round := pe.VoteB.Round; round > pe.VoteA.Round; round-- {
polc, err = evpool.RetrievePOLC(height, round)
if err != nil {
evpool.logger.Error("Failed to retrieve polc for potential amnesia evidence", "err", err, "pae", pe.String())
continue
}
if polc != nil && !polc.IsAbsent() {
evpool.logger.Debug("Found polc for potential amnesia evidence", "polc", polc)
// we should not need to verify it if both the polc and potential amnesia evidence have already
// been verified. We replace the potential amnesia evidence.
ae := types.NewAmnesiaEvidence(pe, polc)
err := evpool.AddEvidence(ae)
if err != nil {
evpool.logger.Error("Failed to create amnesia evidence from potential amnesia evidence", "err", err)
// revert back to processing potential amnesia evidence
exists = false
} else {
evpool.logger.Info("Formed amnesia evidence from own polc", "amnesiaEvidence", ae)
}
break
}
}
// stamp height that the evidence was received
pe.HeightStamp = evpool.State().LastBlockHeight
// b) check if amnesia evidence can be made now or if we need to enact the trial period
if !exists && pe.Primed(1, pe.HeightStamp) {
evpool.logger.Debug("PotentialAmnesiaEvidence can be instantly upgraded")
err := evpool.AddEvidence(types.NewAmnesiaEvidence(pe, types.NewEmptyPOLC()))
if err != nil {
return err
}
} else if !exists && evpool.State().LastBlockHeight+evpool.State().ConsensusParams.Evidence.ProofTrialPeriod <
pe.Height()+evpool.State().ConsensusParams.Evidence.MaxAgeNumBlocks {
// if we can't find a proof of lock change and we know that the trial period will finish before the
// evidence has expired, then we commence the trial period by saving it in the awaiting bucket
pbe, err := types.EvidenceToProto(pe)
if err != nil {
return err
}
evBytes, err := pbe.Marshal()
if err != nil {
return err
}
key := keyAwaitingTrial(pe)
err = evpool.evidenceStore.Set(key, evBytes)
if err != nil {
return err
}
evpool.logger.Debug("Valid potential amnesia evidence has been added. Starting trial period",
"ev", pe)
// keep track of when the next pe has finished the trial period
if evpool.nextEvidenceTrialEndedHeight == -1 {
evpool.nextEvidenceTrialEndedHeight = pe.Height() + evpool.State().ConsensusParams.Evidence.ProofTrialPeriod
}
// add to the broadcast list so it can continue to be gossiped
evpool.evidenceList.PushBack(pe)
}
return nil
}
func evMapKey(ev types.Evidence) string {
return string(ev.Hash())
}
@ -724,11 +800,11 @@ func keyPending(evidence types.Evidence) []byte {
return append([]byte{baseKeyPending}, keySuffix(evidence)...)
}
func keyAwaiting(evidence types.Evidence) []byte {
return append([]byte{baseKeyAwaiting}, keySuffix(evidence)...)
func keyAwaitingTrial(evidence types.Evidence) []byte {
return append([]byte{baseKeyAwaitingTrial}, keySuffix(evidence)...)
}
func keyPOLC(polc types.ProofOfLockChange) []byte {
func keyPOLC(polc *types.ProofOfLockChange) []byte {
return keyPOLCFromHeightAndRound(polc.Height(), polc.Round())
}


+ 132
- 34
evidence/pool_test.go View File

@ -12,6 +12,7 @@ import (
dbm "github.com/tendermint/tm-db"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/tmhash"
"github.com/tendermint/tendermint/libs/bytes"
"github.com/tendermint/tendermint/libs/log"
tmrand "github.com/tendermint/tendermint/libs/rand"
@ -211,10 +212,27 @@ func TestAddingAndPruningPOLC(t *testing.T) {
blockStore = initializeBlockStore(blockStoreDB, state, valAddr)
height = state.ConsensusParams.Evidence.MaxAgeNumBlocks * 2
evidenceTime = time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
firstBlockID = types.BlockID{
Hash: tmrand.Bytes(tmhash.Size),
PartSetHeader: types.PartSetHeader{
Total: 1,
Hash: tmrand.Bytes(tmhash.Size),
},
}
)
val := types.NewMockPV()
voteA := makeVote(1, 1, 0, val.PrivKey.PubKey().Address(), firstBlockID, evidenceTime)
vA := voteA.ToProto()
err := val.SignVote(evidenceChainID, vA)
require.NoError(t, err)
voteA.Signature = vA.Signature
pubKey, _ := types.NewMockPV().GetPubKey()
polc := types.NewMockPOLC(1, evidenceTime, pubKey)
polc := &types.ProofOfLockChange{
Votes: []*types.Vote{voteA},
PubKey: pubKey,
}
pool, err := NewPool(stateDB, evidenceDB, blockStore)
require.NoError(t, err)
@ -227,10 +245,10 @@ func TestAddingAndPruningPOLC(t *testing.T) {
assert.NoError(t, err)
assert.True(t, polc.Equal(newPolc))
// should not be able to retrieve
// should not be able to retrieve because it doesn't exist
emptyPolc, err := pool.RetrievePOLC(2, 1)
assert.Error(t, err)
assert.Equal(t, types.ProofOfLockChange{}, emptyPolc)
assert.NoError(t, err)
assert.Nil(t, emptyPolc)
lastCommit := makeCommit(height-1, valAddr)
block := types.MakeBlock(height, []types.Tx{}, lastCommit, []types.Evidence{})
@ -242,8 +260,8 @@ func TestAddingAndPruningPOLC(t *testing.T) {
pool.Update(block, state)
emptyPolc, err = pool.RetrievePOLC(1, 1)
assert.Error(t, err)
assert.Equal(t, types.ProofOfLockChange{}, emptyPolc)
assert.NoError(t, err)
assert.Nil(t, emptyPolc)
}
@ -284,15 +302,20 @@ func TestRecoverPendingEvidence(t *testing.T) {
assert.True(t, pool.IsPending(goodEvidence))
}
func TestPotentialAmnesiaEvidence(t *testing.T) {
// Comprehensive set of test cases relating to the adding, upgrading and overall
// processing of PotentialAmnesiaEvidence and AmnesiaEvidence
func TestAddingPotentialAmnesiaEvidence(t *testing.T) {
var (
val = types.NewMockPV()
pubKey = val.PrivKey.PubKey()
valSet = &types.ValidatorSet{
val = types.NewMockPV()
val2 = types.NewMockPV()
pubKey = val.PrivKey.PubKey()
pubKey2 = val2.PrivKey.PubKey()
valSet = &types.ValidatorSet{
Validators: []*types.Validator{
val.ExtractIntoValidator(0),
val.ExtractIntoValidator(1),
val2.ExtractIntoValidator(3),
},
Proposer: val.ExtractIntoValidator(0),
Proposer: val.ExtractIntoValidator(1),
}
height = int64(30)
stateDB = initializeStateFromValidatorSet(valSet, height)
@ -318,38 +341,56 @@ func TestPotentialAmnesiaEvidence(t *testing.T) {
evidenceTime = time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
)
// TEST SETUP
pool, err := NewPool(stateDB, evidenceDB, blockStore)
require.NoError(t, err)
pool.SetLogger(log.TestingLogger())
polc := types.NewMockPOLC(25, evidenceTime, pubKey)
err = pool.AddPOLC(polc)
require.NoError(t, err)
_, err = pool.RetrievePOLC(25, 1)
require.NoError(t, err)
voteA := makeVote(25, 0, 0, pubKey.Address(), firstBlockID)
voteA := makeVote(height, 0, 0, pubKey.Address(), firstBlockID, evidenceTime)
vA := voteA.ToProto()
err = val.SignVote(evidenceChainID, vA)
voteA.Signature = vA.Signature
require.NoError(t, err)
voteB := makeVote(25, 1, 0, pubKey.Address(), secondBlockID)
voteB := makeVote(height, 1, 0, pubKey.Address(), secondBlockID, evidenceTime.Add(3*time.Second))
vB := voteB.ToProto()
err = val.SignVote(evidenceChainID, vB)
voteB.Signature = vB.Signature
require.NoError(t, err)
voteC := makeVote(25, 0, 0, pubKey.Address(), firstBlockID)
voteC.Timestamp.Add(1 * time.Second)
voteC := makeVote(height, 2, 0, pubKey.Address(), firstBlockID, evidenceTime.Add(2*time.Second))
vC := voteC.ToProto()
err = val.SignVote(evidenceChainID, vC)
voteC.Signature = vC.Signature
require.NoError(t, err)
ev := types.PotentialAmnesiaEvidence{
ev := &types.PotentialAmnesiaEvidence{
VoteA: voteA,
VoteB: voteB,
}
polc := &types.ProofOfLockChange{
Votes: []*types.Vote{voteB},
PubKey: pubKey2,
}
err = pool.AddPOLC(polc)
require.NoError(t, err)
polc, err = pool.RetrievePOLC(height, 1)
require.NoError(t, err)
require.NotEmpty(t, polc)
secondValVote := makeVote(height, 1, 0, pubKey2.Address(), secondBlockID, evidenceTime.Add(1*time.Second))
vv2 := secondValVote.ToProto()
err = val2.SignVote(evidenceChainID, vv2)
require.NoError(t, err)
secondValVote.Signature = vv2.Signature
validPolc := &types.ProofOfLockChange{
Votes: []*types.Vote{secondValVote},
PubKey: pubKey,
}
// CASE A
pool.logger.Info("CASE A")
// we expect the evidence pool to find the polc but log an error as the polc is not valid -> vote was
// not from a validator in this set. However, an error isn't thrown because the evidence pool
// should still be able to save the regular potential amnesia evidence.
@ -358,33 +399,89 @@ func TestPotentialAmnesiaEvidence(t *testing.T) {
// evidence requires trial period until it is available -> we expect no evidence to be returned
assert.Equal(t, 0, len(pool.PendingEvidence(1)))
assert.True(t, pool.IsOnTrial(ev))
nextHeight := pool.nextEvidenceTrialEndedHeight
assert.Greater(t, nextHeight, int64(0))
// CASE B
pool.logger.Info("CASE B")
// evidence is not ready to be upgraded so we return the height we expect the evidence to be.
nextHeight = pool.upgradePotentialAmnesiaEvidence()
assert.Equal(t, height+pool.state.ConsensusParams.Evidence.ProofTrialPeriod, nextHeight)
// CASE C
pool.logger.Info("CASE C")
// now evidence is ready to be upgraded to amnesia evidence -> we expect -1 to be the next height as their is
// no more pending potential amnesia evidence left
pool.state.LastBlockHeight = nextHeight
nextHeight = pool.upgradePotentialAmnesiaEvidence()
assert.Equal(t, int64(-1), nextHeight)
lastCommit := makeCommit(height+1, pubKey.Address())
block := types.MakeBlock(height+2, []types.Tx{}, lastCommit, []types.Evidence{})
state.LastBlockHeight = height + 2
pool.Update(block, state)
assert.Equal(t, int64(-1), pool.nextEvidenceTrialEndedHeight)
assert.Equal(t, 1, len(pool.PendingEvidence(1)))
// CASE D
pool.logger.Info("CASE D")
// evidence of voting back in the past which is instantly punishable -> amnesia evidence is made directly
voteA.Timestamp.Add(1 * time.Second)
ev2 := types.PotentialAmnesiaEvidence{
VoteA: voteB,
VoteB: voteC,
ev2 := &types.PotentialAmnesiaEvidence{
VoteA: voteC,
VoteB: voteB,
}
err = pool.AddEvidence(ev2)
assert.NoError(t, err)
expectedAe := &types.AmnesiaEvidence{
PotentialAmnesiaEvidence: ev2,
Polc: types.NewEmptyPOLC(),
}
assert.True(t, pool.IsPending(expectedAe))
assert.Equal(t, 2, len(pool.AllPendingEvidence()))
// CASE E
pool.logger.Info("CASE E")
// test for receiving amnesia evidence
ae := types.NewAmnesiaEvidence(ev, types.NewEmptyPOLC())
// we need to run the trial period ourselves so amnesia evidence should not be added, instead
// we should extract out the potential amnesia evidence and trying to add that before realising
// that we already have it -> no error
err = pool.AddEvidence(ae)
assert.NoError(t, err)
assert.Equal(t, 2, len(pool.AllPendingEvidence()))
voteD := makeVote(height, 2, 0, pubKey.Address(), firstBlockID, evidenceTime.Add(4*time.Second))
vD := voteD.ToProto()
err = val.SignVote(evidenceChainID, vD)
require.NoError(t, err)
voteD.Signature = vD.Signature
// CASE F
pool.logger.Info("CASE F")
// a new amnesia evidence is seen. It has an empty polc so we should extract the potential amnesia evidence
// and start our own trial
newPe := types.NewPotentialAmnesiaEvidence(voteB, voteD)
newAe := types.NewAmnesiaEvidence(newPe, types.NewEmptyPOLC())
err = pool.AddEvidence(newAe)
assert.NoError(t, err)
assert.Equal(t, 2, len(pool.AllPendingEvidence()))
assert.True(t, pool.IsOnTrial(newPe))
// CASE G
pool.logger.Info("CASE G")
// Finally, we receive an amnesia evidence containing a valid polc for an earlier potential amnesia evidence
// that we have already upgraded to. We should ad this new amnesia evidence in replace of the prior
// amnesia evidence with an empty polc that we have
aeWithPolc := &types.AmnesiaEvidence{
PotentialAmnesiaEvidence: ev,
Polc: validPolc,
}
err = pool.AddEvidence(aeWithPolc)
assert.NoError(t, err)
assert.True(t, pool.IsPending(aeWithPolc))
assert.Equal(t, 2, len(pool.AllPendingEvidence()))
t.Log(pool.AllPendingEvidence())
}
@ -465,13 +562,14 @@ func makeCommit(height int64, valAddr []byte) *types.Commit {
return types.NewCommit(height, 0, types.BlockID{}, commitSigs)
}
func makeVote(height int64, round, index int32, addr bytes.HexBytes, blockID types.BlockID) *types.Vote {
func makeVote(height int64, round, index int32, addr bytes.HexBytes,
blockID types.BlockID, time time.Time) *types.Vote {
return &types.Vote{
Type: tmproto.SignedMsgType(2),
Height: height,
Round: round,
BlockID: blockID,
Timestamp: time.Now(),
Timestamp: time,
ValidatorAddress: addr,
ValidatorIndex: index,
}


+ 2
- 2
light/client.go View File

@ -975,7 +975,7 @@ func (c *Client) compareNewHeaderWithWitnesses(h *types.SignedHeader) error {
headerMatched = true
case ErrConflictingHeaders: // potential fork
c.logger.Error(err.Error(), "witness", e.Witness)
c.sendConflictingHeadersEvidence(types.ConflictingHeadersEvidence{H1: h, H2: e.H2})
c.sendConflictingHeadersEvidence(&types.ConflictingHeadersEvidence{H1: h, H2: e.H2})
lastErrConfHeaders = e
case errBadWitness:
c.logger.Error(err.Error(), "witness", c.witnesses[e.WitnessIndex])
@ -1188,7 +1188,7 @@ func (c *Client) validateValidatorSet(vals *types.ValidatorSet) error {
//
// 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) {
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)


+ 1
- 1
light/client_test.go View File

@ -1083,7 +1083,7 @@ func TestClientReportsConflictingHeadersEvidence(t *testing.T) {
}
// Check evidence was sent to both full nodes.
ev := types.ConflictingHeadersEvidence{H1: h2, H2: altH2}
ev := &types.ConflictingHeadersEvidence{H1: h2, H2: altH2}
assert.True(t, fullNode2.HasEvidence(ev))
assert.True(t, fullNode.HasEvidence(ev))
}


+ 1
- 1
rpc/client/evidence_test.go View File

@ -212,7 +212,7 @@ func TestBroadcastEvidence_ConflictingHeadersEvidence(t *testing.T) {
t.Logf("h1 AppHash: %X", h1.AppHash)
t.Logf("h2 AppHash: %X", h2.AppHash)
ev := types.ConflictingHeadersEvidence{
ev := &types.ConflictingHeadersEvidence{
H1: &h1.SignedHeader,
H2: h2,
}


+ 7
- 6
state/validation.go View File

@ -142,15 +142,16 @@ func validateBlock(evidencePool EvidencePool, stateDB dbm.DB, state State, block
continue
}
}
// if we don't already have amnesia evidence we need to add it to start our own timer unless
// if we don't already have amnesia evidence we need to add it to start our own trial period unless
// a) a valid polc has already been attached
// b) the accused node voted back on an earlier round
if ae, ok := ev.(types.AmnesiaEvidence); ok && ae.Polc.IsAbsent() && ae.PotentialAmnesiaEvidence.VoteA.Round <
if ae, ok := ev.(*types.AmnesiaEvidence); ok && ae.Polc.IsAbsent() && ae.PotentialAmnesiaEvidence.VoteA.Round <
ae.PotentialAmnesiaEvidence.VoteB.Round {
if err := evidencePool.AddEvidence(ae); err != nil {
if err := evidencePool.AddEvidence(ae.PotentialAmnesiaEvidence); err != nil {
return types.NewErrEvidenceInvalid(ev,
fmt.Errorf("unknown amnesia evidence, trying to add to evidence pool, err: %w", err))
}
return types.NewErrEvidenceInvalid(ev, errors.New("amnesia evidence is new and hasn't undergone trial period yet"))
}
var header *types.Header
@ -208,7 +209,7 @@ func VerifyEvidence(stateDB dbm.DB, state State, evidence types.Evidence, commit
state.LastBlockTime.Add(evidenceParams.MaxAgeDuration),
)
}
if ev, ok := evidence.(types.LunaticValidatorEvidence); ok {
if ev, ok := evidence.(*types.LunaticValidatorEvidence); ok {
if err := ev.VerifyHeader(committedHeader); err != nil {
return err
}
@ -227,7 +228,7 @@ func VerifyEvidence(stateDB dbm.DB, state State, evidence types.Evidence, commit
// 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 {
if phve, ok := evidence.(*types.PhantomValidatorEvidence); ok {
// confirm that it hasn't been forged
_, val = valset.GetByAddress(addr)
@ -253,7 +254,7 @@ func VerifyEvidence(stateDB dbm.DB, state State, evidence types.Evidence, commit
return fmt.Errorf("phantom validator %X not found", addr)
}
} else {
if ae, ok := evidence.(types.AmnesiaEvidence); ok {
if ae, ok := evidence.(*types.AmnesiaEvidence); ok {
// check the validator set against the polc to make sure that a majority of valid votes was reached
if !ae.Polc.IsAbsent() {
err = ae.Polc.ValidateVotes(valset, state.ChainID)


+ 76
- 28
state/validation_test.go View File

@ -1,7 +1,6 @@
package state_test
import (
"fmt"
"testing"
"time"
@ -359,7 +358,7 @@ var blockID = types.BlockID{
},
}
func TestValidateAmnesiaEvidence(t *testing.T) {
func TestValidateUnseenAmnesiaEvidence(t *testing.T) {
var height int64 = 1
state, stateDB, vals := makeState(1, int(height))
addr, val := state.Validators.GetByIndex(0)
@ -373,18 +372,20 @@ func TestValidateAmnesiaEvidence(t *testing.T) {
err = vals[val.Address.String()].SignVote(chainID, vB)
voteB.Signature = vB.Signature
require.NoError(t, err)
ae := types.AmnesiaEvidence{
PotentialAmnesiaEvidence: types.PotentialAmnesiaEvidence{
VoteA: voteA,
VoteB: voteB,
},
Polc: types.EmptyPOLC(),
pe := &types.PotentialAmnesiaEvidence{
VoteA: voteA,
VoteB: voteB,
}
ae := &types.AmnesiaEvidence{
PotentialAmnesiaEvidence: pe,
Polc: types.NewEmptyPOLC(),
}
evpool := &mocks.EvidencePool{}
evpool.On("IsPending", ae).Return(false)
evpool.On("IsCommitted", ae).Return(false)
evpool.On("AddEvidence", ae).Return(fmt.Errorf("test error"))
evpool.On("AddEvidence", ae).Return(nil)
evpool.On("AddEvidence", pe).Return(nil)
blockExec := sm.NewBlockExecutor(
stateDB, log.TestingLogger(),
@ -396,11 +397,58 @@ func TestValidateAmnesiaEvidence(t *testing.T) {
block.Evidence.Evidence = []types.Evidence{ae}
block.EvidenceHash = block.Evidence.Hash()
err = blockExec.ValidateBlock(state, block)
errMsg := "Invalid evidence: unknown amnesia evidence, trying to add to evidence pool, err: test error"
// if we don't have this evidence and it is has an empty polc then we expect to
// start our own trial period first
errMsg := "Invalid evidence: amnesia evidence is new and hasn't undergone trial period yet."
if assert.Error(t, err) {
assert.Equal(t, err.Error()[:len(errMsg)], errMsg)
assert.Equal(t, errMsg, err.Error()[:len(errMsg)])
}
}
// Amnesia Evidence can be directly approved without needing to undergo the trial period
func TestValidatePrimedAmnesiaEvidence(t *testing.T) {
var height int64 = 1
state, stateDB, vals := makeState(1, int(height))
addr, val := state.Validators.GetByIndex(0)
voteA := makeVote(height, 1, 0, addr, blockID)
voteA.Timestamp = time.Now().Add(1 * time.Minute)
vA := voteA.ToProto()
err := vals[val.Address.String()].SignVote(chainID, vA)
require.NoError(t, err)
voteA.Signature = vA.Signature
voteB := makeVote(height, 2, 0, addr, types.BlockID{})
vB := voteB.ToProto()
err = vals[val.Address.String()].SignVote(chainID, vB)
voteB.Signature = vB.Signature
require.NoError(t, err)
pe := &types.PotentialAmnesiaEvidence{
VoteA: voteB,
VoteB: voteA,
}
ae := &types.AmnesiaEvidence{
PotentialAmnesiaEvidence: pe,
Polc: types.NewEmptyPOLC(),
}
evpool := &mocks.EvidencePool{}
evpool.On("IsPending", ae).Return(false)
evpool.On("IsCommitted", ae).Return(false)
evpool.On("AddEvidence", ae).Return(nil)
evpool.On("AddEvidence", pe).Return(nil)
blockExec := sm.NewBlockExecutor(
stateDB, log.TestingLogger(),
nil,
nil,
evpool)
// A block with a couple pieces of evidence passes.
block := makeBlock(state, height)
block.Evidence.Evidence = []types.Evidence{ae}
block.EvidenceHash = block.Evidence.Hash()
err = blockExec.ValidateBlock(state, block)
// No error because this type of amnesia evidence is punishable
// without the need of a trial period
assert.NoError(t, err)
}
func TestVerifyEvidenceWrongAddress(t *testing.T) {
@ -459,13 +507,13 @@ func TestVerifyEvidenceWithAmnesiaEvidence(t *testing.T) {
voteC.Signature = vC.Signature
require.NoError(t, err)
//var ae types.Evidence
badAe := types.AmnesiaEvidence{
PotentialAmnesiaEvidence: types.PotentialAmnesiaEvidence{
badAe := &types.AmnesiaEvidence{
PotentialAmnesiaEvidence: &types.PotentialAmnesiaEvidence{
VoteA: voteA,
VoteB: voteB,
},
Polc: types.ProofOfLockChange{
Votes: []types.Vote{*voteC},
Polc: &types.ProofOfLockChange{
Votes: []*types.Vote{voteC},
PubKey: val.PubKey,
},
}
@ -487,25 +535,25 @@ func TestVerifyEvidenceWithAmnesiaEvidence(t *testing.T) {
voteE.Signature = vE.Signature
require.NoError(t, err)
goodAe := types.AmnesiaEvidence{
PotentialAmnesiaEvidence: types.PotentialAmnesiaEvidence{
goodAe := &types.AmnesiaEvidence{
PotentialAmnesiaEvidence: &types.PotentialAmnesiaEvidence{
VoteA: voteA,
VoteB: voteB,
},
Polc: types.ProofOfLockChange{
Votes: []types.Vote{*voteC, *voteD, *voteE},
Polc: &types.ProofOfLockChange{
Votes: []*types.Vote{voteC, voteD, voteE},
PubKey: val.PubKey,
},
}
err = sm.VerifyEvidence(stateDB, state, goodAe, nil)
assert.NoError(t, err)
goodAe = types.AmnesiaEvidence{
PotentialAmnesiaEvidence: types.PotentialAmnesiaEvidence{
goodAe = &types.AmnesiaEvidence{
PotentialAmnesiaEvidence: &types.PotentialAmnesiaEvidence{
VoteA: voteA,
VoteB: voteB,
},
Polc: types.EmptyPOLC(),
Polc: types.NewEmptyPOLC(),
}
err = sm.VerifyEvidence(stateDB, state, goodAe, nil)
assert.NoError(t, err)
@ -537,7 +585,7 @@ func TestVerifyEvidenceWithLunaticValidatorEvidence(t *testing.T) {
err := vals[val.Address.String()].SignVote(chainID, v)
vote.Signature = v.Signature
require.NoError(t, err)
ev := types.LunaticValidatorEvidence{
ev := &types.LunaticValidatorEvidence{
Header: h,
Vote: vote,
InvalidHeaderField: "ConsensusHash",
@ -559,7 +607,7 @@ func TestVerifyEvidenceWithPhantomValidatorEvidence(t *testing.T) {
err := vals[val.Address.String()].SignVote(chainID, v)
vote.Signature = v.Signature
require.NoError(t, err)
ev := types.PhantomValidatorEvidence{
ev := &types.PhantomValidatorEvidence{
Vote: vote,
LastHeightValidatorWasInSet: 1,
}
@ -577,7 +625,7 @@ func TestVerifyEvidenceWithPhantomValidatorEvidence(t *testing.T) {
err = privVal.SignVote(chainID, v2)
vote2.Signature = v2.Signature
require.NoError(t, err)
ev = types.PhantomValidatorEvidence{
ev = &types.PhantomValidatorEvidence{
Vote: vote2,
LastHeightValidatorWasInSet: 1,
}
@ -588,7 +636,7 @@ func TestVerifyEvidenceWithPhantomValidatorEvidence(t *testing.T) {
assert.Equal(t, "last time validator was in the set at height 1, min: 2", err.Error())
}
ev = types.PhantomValidatorEvidence{
ev = &types.PhantomValidatorEvidence{
Vote: vote2,
LastHeightValidatorWasInSet: 2,
}
@ -615,7 +663,7 @@ func TestVerifyEvidenceWithPhantomValidatorEvidence(t *testing.T) {
require.NoError(t, err)
stateDB.Set(valKey, bz)
ev = types.PhantomValidatorEvidence{
ev = &types.PhantomValidatorEvidence{
Vote: vote2,
LastHeightValidatorWasInSet: 2,
}


+ 2
- 2
types/block.go View File

@ -90,11 +90,11 @@ func (b *Block) ValidateBasic() error {
// NOTE: b.Evidence.Evidence may be nil, but we're just looping.
for i, ev := range b.Evidence.Evidence {
switch ev.(type) {
case *ConflictingHeadersEvidence, ConflictingHeadersEvidence:
case *ConflictingHeadersEvidence:
// ConflictingHeadersEvidence must be broken up in pieces and never
// committed as a single piece.
return fmt.Errorf("found ConflictingHeadersEvidence (#%d)", i)
case *PotentialAmnesiaEvidence, PotentialAmnesiaEvidence:
case *PotentialAmnesiaEvidence:
// PotentialAmnesiaEvidence does not contribute to anything on its own, so
// reject it as well.
return fmt.Errorf("found PotentialAmnesiaEvidence (#%d)", i)


+ 287
- 267
types/evidence.go
File diff suppressed because it is too large
View File


+ 116
- 94
types/evidence_test.go View File

@ -222,11 +222,7 @@ func TestLunaticValidatorEvidence(t *testing.T) {
header.Time = bTime
ev := &LunaticValidatorEvidence{
Header: header,
Vote: vote,
InvalidHeaderField: "AppHash",
}
ev := NewLunaticValidatorEvidence(header, vote, "AppHash")
assert.Equal(t, header.Height, ev.Height())
assert.Equal(t, defaultVoteTime, ev.Time())
@ -253,10 +249,7 @@ func TestPhantomValidatorEvidence(t *testing.T) {
vote = makeVote(t, val, header.ChainID, 0, header.Height, 0, 2, blockID, defaultVoteTime)
)
ev := &PhantomValidatorEvidence{
Vote: vote,
LastHeightValidatorWasInSet: header.Height - 1,
}
ev := NewPhantomValidatorEvidence(vote, header.Height-1)
assert.Equal(t, header.Height, ev.Height())
assert.Equal(t, defaultVoteTime, ev.Time())
@ -315,17 +308,17 @@ func TestConflictingHeadersEvidence(t *testing.T) {
}, height, 1, voteSet2, vals, time.Now())
require.NoError(t, err)
ev := &ConflictingHeadersEvidence{
H1: &SignedHeader{
Header: header1,
Commit: commit1,
},
H2: &SignedHeader{
Header: header2,
Commit: commit2,
},
h1 := &SignedHeader{
Header: header1,
Commit: commit1,
}
h2 := &SignedHeader{
Header: header2,
Commit: commit2,
}
ev := NewConflictingHeadersEvidence(h1, h2)
assert.Panics(t, func() {
ev.Address()
})
@ -356,13 +349,11 @@ func TestPotentialAmnesiaEvidence(t *testing.T) {
blockID = makeBlockID(tmhash.Sum([]byte("blockhash")), math.MaxInt32, tmhash.Sum([]byte("partshash")))
blockID2 = makeBlockID(tmhash.Sum([]byte("blockhash2")), math.MaxInt32, tmhash.Sum([]byte("partshash")))
vote1 = makeVote(t, val, chainID, 0, height, 0, 2, blockID, defaultVoteTime)
vote2 = makeVote(t, val, chainID, 0, height, 1, 2, blockID2, defaultVoteTime.Add(1*time.Minute))
vote2 = makeVote(t, val, chainID, 0, height, 1, 2, blockID2, defaultVoteTime.Add(1*time.Second))
vote3 = makeVote(t, val, chainID, 0, height, 2, 2, blockID, defaultVoteTime)
)
ev := &PotentialAmnesiaEvidence{
VoteA: vote1,
VoteB: vote2,
}
ev := NewPotentialAmnesiaEvidence(vote1, vote2)
assert.Equal(t, height, ev.Height())
assert.Equal(t, vote2.Timestamp, ev.Time())
@ -379,6 +370,35 @@ func TestPotentialAmnesiaEvidence(t *testing.T) {
assert.True(t, ev.Equal(ev))
assert.NoError(t, ev.ValidateBasic())
assert.NotEmpty(t, ev.String())
ev2 := &PotentialAmnesiaEvidence{
VoteA: vote1,
VoteB: vote2,
HeightStamp: 5,
}
assert.True(t, ev.Equal(ev2))
assert.Equal(t, ev.Hash(), ev2.Hash())
ev3 := &PotentialAmnesiaEvidence{
VoteA: vote2,
VoteB: vote1,
}
assert.Error(t, ev3.ValidateBasic())
ev3 = NewPotentialAmnesiaEvidence(vote2, vote1)
assert.True(t, ev3.Equal(ev))
ev4 := &PotentialAmnesiaEvidence{
VoteA: vote3,
VoteB: vote2,
}
assert.NoError(t, ev4.ValidateBasic())
assert.NotEqual(t, ev.Hash(), ev4.Hash())
assert.False(t, ev.Equal(ev4))
}
func TestProofOfLockChange(t *testing.T) {
@ -390,7 +410,8 @@ func TestProofOfLockChange(t *testing.T) {
voteSet, valSet, privValidators, blockID := buildVoteSet(height, 1, 3, 7, 0, tmproto.PrecommitType)
pubKey, err := privValidators[7].GetPubKey()
require.NoError(t, err)
polc := makePOLCFromVoteSet(voteSet, pubKey, blockID)
polc, err := NewPOLCFromVoteSet(voteSet, pubKey, blockID)
assert.NoError(t, err)
assert.Equal(t, height, polc.Height())
assert.NoError(t, polc.ValidateBasic())
@ -410,34 +431,34 @@ func TestProofOfLockChange(t *testing.T) {
assert.Error(t, err)
// test validate basic on a set of bad cases
var badPOLCs []ProofOfLockChange
var badPOLCs []*ProofOfLockChange
// 2: node has already voted in next round
pubKey, err = privValidators[0].GetPubKey()
require.NoError(t, err)
polc2 := makePOLCFromVoteSet(voteSet, pubKey, blockID)
polc2 := newPOLCFromVoteSet(voteSet, pubKey, blockID)
badPOLCs = append(badPOLCs, polc2)
// 3: one vote was from a different round
voteSet, _, privValidators, blockID = buildVoteSet(height, 1, 3, 7, 0, tmproto.PrecommitType)
pubKey, err = privValidators[7].GetPubKey()
require.NoError(t, err)
polc = makePOLCFromVoteSet(voteSet, pubKey, blockID)
polc = newPOLCFromVoteSet(voteSet, pubKey, blockID)
badVote := makeVote(t, privValidators[8], chainID, 8, height, 2, 2, blockID, defaultVoteTime)
polc.Votes = append(polc.Votes, *badVote)
polc.Votes = append(polc.Votes, badVote)
badPOLCs = append(badPOLCs, polc)
// 4: one vote was from a different height
polc = makePOLCFromVoteSet(voteSet, pubKey, blockID)
polc = newPOLCFromVoteSet(voteSet, pubKey, blockID)
badVote = makeVote(t, privValidators[8], chainID, 8, height+1, 1, 2, blockID, defaultVoteTime)
polc.Votes = append(polc.Votes, *badVote)
polc.Votes = append(polc.Votes, badVote)
badPOLCs = append(badPOLCs, polc)
// 5: one vote was from a different vote type
polc = makePOLCFromVoteSet(voteSet, pubKey, blockID)
polc = newPOLCFromVoteSet(voteSet, pubKey, blockID)
badVote = makeVote(t, privValidators[8], chainID, 8, height, 1, 1, blockID, defaultVoteTime)
polc.Votes = append(polc.Votes, *badVote)
polc.Votes = append(polc.Votes, badVote)
badPOLCs = append(badPOLCs, polc)
// 5: one of the votes was for a nil block
polc = makePOLCFromVoteSet(voteSet, pubKey, blockID)
polc = newPOLCFromVoteSet(voteSet, pubKey, blockID)
badVote = makeVote(t, privValidators[8], chainID, 8, height, 1, 2, BlockID{}, defaultVoteTime)
polc.Votes = append(polc.Votes, *badVote)
polc.Votes = append(polc.Votes, badVote)
badPOLCs = append(badPOLCs, polc)
for idx, polc := range badPOLCs {
@ -467,17 +488,17 @@ func TestAmnesiaEvidence(t *testing.T) {
vote2 = makeVote(t, val, chainID, 7, height, 1, 2, blockID,
time.Now().Add(time.Second))
vote3 = makeVote(t, val, chainID, 7, height, 2, 2, blockID2, time.Now())
polc = makePOLCFromVoteSet(voteSet, pubKey, blockID)
polc = newPOLCFromVoteSet(voteSet, pubKey, blockID)
)
require.False(t, polc.IsAbsent())
pe := PotentialAmnesiaEvidence{
pe := &PotentialAmnesiaEvidence{
VoteA: vote1,
VoteB: vote2,
}
emptyAmnesiaEvidence := MakeAmnesiaEvidence(pe, EmptyPOLC())
emptyAmnesiaEvidence := NewAmnesiaEvidence(pe, NewEmptyPOLC())
assert.NoError(t, emptyAmnesiaEvidence.ValidateBasic())
violated, reason := emptyAmnesiaEvidence.ViolatedConsensus()
@ -486,7 +507,7 @@ func TestAmnesiaEvidence(t *testing.T) {
}
assert.NoError(t, emptyAmnesiaEvidence.Verify(chainID, pubKey))
completeAmnesiaEvidence := MakeAmnesiaEvidence(pe, polc)
completeAmnesiaEvidence := NewAmnesiaEvidence(pe, polc)
assert.NoError(t, completeAmnesiaEvidence.ValidateBasic())
violated, reason = completeAmnesiaEvidence.ViolatedConsensus()
@ -497,40 +518,41 @@ func TestAmnesiaEvidence(t *testing.T) {
assert.NoError(t, completeAmnesiaEvidence.Polc.ValidateVotes(valSet, chainID))
assert.True(t, completeAmnesiaEvidence.Equal(emptyAmnesiaEvidence))
assert.Equal(t, completeAmnesiaEvidence.Hash(), emptyAmnesiaEvidence.Hash())
assert.NotEmpty(t, completeAmnesiaEvidence.Hash())
assert.NotEmpty(t, completeAmnesiaEvidence.Bytes())
pe2 := PotentialAmnesiaEvidence{
pe2 := &PotentialAmnesiaEvidence{
VoteA: vote3,
VoteB: vote2,
}
// validator has incorrectly voted for a previous round after voting for a later round
ae := MakeAmnesiaEvidence(pe2, EmptyPOLC())
ae := NewAmnesiaEvidence(pe2, NewEmptyPOLC())
assert.NoError(t, ae.ValidateBasic())
violated, reason = ae.ViolatedConsensus()
if assert.True(t, violated) {
assert.Equal(t, reason, "validator went back and voted on a previous round")
}
var badAE []AmnesiaEvidence
var badAE []*AmnesiaEvidence
// 1) Polc is at an incorrect height
voteSet, _, _ = buildVoteSetForBlock(height+1, 1, 2, 7, 0, tmproto.PrecommitType, blockID)
polc = makePOLCFromVoteSet(voteSet, pubKey, blockID)
badAE = append(badAE, MakeAmnesiaEvidence(pe, polc))
polc = newPOLCFromVoteSet(voteSet, pubKey, blockID)
badAE = append(badAE, NewAmnesiaEvidence(pe, polc))
// 2) Polc is of a later round
voteSet, _, _ = buildVoteSetForBlock(height, 2, 2, 7, 0, tmproto.PrecommitType, blockID)
polc = makePOLCFromVoteSet(voteSet, pubKey, blockID)
badAE = append(badAE, MakeAmnesiaEvidence(pe, polc))
polc = newPOLCFromVoteSet(voteSet, pubKey, blockID)
badAE = append(badAE, NewAmnesiaEvidence(pe, polc))
// 3) Polc has a different public key
voteSet, _, privValidators = buildVoteSetForBlock(height, 1, 2, 7, 0, tmproto.PrecommitType, blockID)
pubKey2, _ := privValidators[7].GetPubKey()
polc = makePOLCFromVoteSet(voteSet, pubKey2, blockID)
badAE = append(badAE, MakeAmnesiaEvidence(pe, polc))
polc = newPOLCFromVoteSet(voteSet, pubKey2, blockID)
badAE = append(badAE, NewAmnesiaEvidence(pe, polc))
// 4) Polc has a different block ID
voteSet, _, _, blockID = buildVoteSet(height, 1, 2, 7, 0, tmproto.PrecommitType)
polc = makePOLCFromVoteSet(voteSet, pubKey, blockID)
badAE = append(badAE, MakeAmnesiaEvidence(pe, polc))
polc = newPOLCFromVoteSet(voteSet, pubKey, blockID)
badAE = append(badAE, NewAmnesiaEvidence(pe, polc))
for idx, ae := range badAE {
t.Log(ae.ValidateBasic())
@ -638,58 +660,56 @@ func TestEvidenceProto(t *testing.T) {
}
tests := []struct {
testName string
evidence Evidence
wantErr bool
wantErr2 bool
testName string
evidence Evidence
toProtoErr bool
fromProtoErr bool
}{
{"&DuplicateVoteEvidence empty fail", &DuplicateVoteEvidence{}, false, true},
{"&DuplicateVoteEvidence nil voteB", &DuplicateVoteEvidence{VoteA: v, VoteB: nil}, false, true},
{"&DuplicateVoteEvidence nil voteA", &DuplicateVoteEvidence{VoteA: nil, VoteB: v}, false, true},
{"&DuplicateVoteEvidence success", &DuplicateVoteEvidence{VoteA: v2, VoteB: v}, false, false},
{"&ConflictingHeadersEvidence empty fail", &ConflictingHeadersEvidence{}, false, true},
{"&ConflictingHeadersEvidence nil H2", &ConflictingHeadersEvidence{H1: h1, H2: nil}, false, true},
{"&ConflictingHeadersEvidence nil H1", &ConflictingHeadersEvidence{H1: nil, H2: h2}, false, true},
{"ConflictingHeadersEvidence empty fail", ConflictingHeadersEvidence{}, false, true},
{"ConflictingHeadersEvidence nil H2", ConflictingHeadersEvidence{H1: h1, H2: nil}, false, true},
{"ConflictingHeadersEvidence nil H1", ConflictingHeadersEvidence{H1: nil, H2: h2}, false, true},
{"ConflictingHeadersEvidence success", ConflictingHeadersEvidence{H1: h1, H2: h2}, false, false},
{"LunaticValidatorEvidence empty fail", LunaticValidatorEvidence{}, false, true},
{"LunaticValidatorEvidence only header fail", LunaticValidatorEvidence{Header: header1}, false, true},
{"LunaticValidatorEvidence only vote fail", LunaticValidatorEvidence{Vote: v}, false, true},
{"LunaticValidatorEvidence header & vote fail", LunaticValidatorEvidence{Header: header1, Vote: v}, false, true},
{"LunaticValidatorEvidence success", LunaticValidatorEvidence{Header: header1,
{"nil fail", nil, true, true},
{"DuplicateVoteEvidence empty fail", &DuplicateVoteEvidence{}, false, true},
{"DuplicateVoteEvidence nil voteB", &DuplicateVoteEvidence{VoteA: v, VoteB: nil}, false, true},
{"DuplicateVoteEvidence nil voteA", &DuplicateVoteEvidence{VoteA: nil, VoteB: v}, false, true},
{"DuplicateVoteEvidence success", &DuplicateVoteEvidence{VoteA: v2, VoteB: v}, false, false},
{"ConflictingHeadersEvidence empty fail", &ConflictingHeadersEvidence{}, false, true},
{"ConflictingHeadersEvidence nil H2", &ConflictingHeadersEvidence{H1: h1, H2: nil}, false, true},
{"ConflictingHeadersEvidence nil H1", &ConflictingHeadersEvidence{H1: nil, H2: h2}, false, true},
{"ConflictingHeadersEvidence success", &ConflictingHeadersEvidence{H1: h1, H2: h2}, false, false},
{"LunaticValidatorEvidence success", &LunaticValidatorEvidence{Header: header1,
Vote: v, InvalidHeaderField: "ValidatorsHash"}, false, true},
{"&LunaticValidatorEvidence empty fail", &LunaticValidatorEvidence{}, false, true},
{"LunaticValidatorEvidence only header fail", &LunaticValidatorEvidence{Header: header1}, false, true},
{"LunaticValidatorEvidence only vote fail", &LunaticValidatorEvidence{Vote: v}, false, true},
{"LunaticValidatorEvidence header & vote fail", &LunaticValidatorEvidence{Header: header1, Vote: v}, false, true},
{"&LunaticValidatorEvidence empty fail", &LunaticValidatorEvidence{}, false, true},
{"PotentialAmnesiaEvidence empty fail", PotentialAmnesiaEvidence{}, false, true},
{"PotentialAmnesiaEvidence nil VoteB", PotentialAmnesiaEvidence{VoteA: v, VoteB: nil}, false, true},
{"PotentialAmnesiaEvidence nil VoteA", PotentialAmnesiaEvidence{VoteA: nil, VoteB: v2}, false, true},
{"&PotentialAmnesiaEvidence empty fail", &PotentialAmnesiaEvidence{}, false, true},
{"&PotentialAmnesiaEvidence nil VoteB", &PotentialAmnesiaEvidence{VoteA: v, VoteB: nil}, false, true},
{"&PotentialAmnesiaEvidence nil VoteA", &PotentialAmnesiaEvidence{VoteA: nil, VoteB: v2}, false, true},
{"&PotentialAmnesiaEvidence success", &PotentialAmnesiaEvidence{VoteA: v2, VoteB: v}, false, false},
{"&PhantomValidatorEvidence empty fail", &PhantomValidatorEvidence{}, false, true},
{"&PhantomValidatorEvidence nil LastHeightValidatorWasInSet", &PhantomValidatorEvidence{Vote: v}, false, true},
{"&PhantomValidatorEvidence nil Vote", &PhantomValidatorEvidence{LastHeightValidatorWasInSet: 2}, false, true},
{"PhantomValidatorEvidence success", PhantomValidatorEvidence{Vote: v2, LastHeightValidatorWasInSet: 2},
{"LunaticValidatorEvidence empty fail", &LunaticValidatorEvidence{}, false, true},
{"PotentialAmnesiaEvidence empty fail", &PotentialAmnesiaEvidence{}, false, true},
{"PotentialAmnesiaEvidence nil VoteB", &PotentialAmnesiaEvidence{VoteA: v, VoteB: nil}, false, true},
{"PotentialAmnesiaEvidence nil VoteA", &PotentialAmnesiaEvidence{VoteA: nil, VoteB: v2}, false, true},
{"PotentialAmnesiaEvidence success", &PotentialAmnesiaEvidence{VoteA: v2, VoteB: v}, false, false},
{"PhantomValidatorEvidence empty fail", &PhantomValidatorEvidence{}, false, true},
{"PhantomValidatorEvidence nil LastHeightValidatorWasInSet", &PhantomValidatorEvidence{Vote: v}, false, true},
{"PhantomValidatorEvidence nil Vote", &PhantomValidatorEvidence{LastHeightValidatorWasInSet: 2}, false, true},
{"PhantomValidatorEvidence success", &PhantomValidatorEvidence{Vote: v2, LastHeightValidatorWasInSet: 2},
false, false},
{"AmnesiaEvidence nil ProofOfLockChange", &AmnesiaEvidence{PotentialAmnesiaEvidence: &PotentialAmnesiaEvidence{},
Polc: NewEmptyPOLC()}, false, true},
{"AmnesiaEvidence nil Polc",
&AmnesiaEvidence{PotentialAmnesiaEvidence: &PotentialAmnesiaEvidence{VoteA: v2, VoteB: v},
Polc: &ProofOfLockChange{}}, false, false},
{"AmnesiaEvidence success", &AmnesiaEvidence{PotentialAmnesiaEvidence: &PotentialAmnesiaEvidence{VoteA: v2, VoteB: v},
Polc: NewEmptyPOLC()}, false, false},
}
for _, tt := range tests {
tt := tt
t.Run(tt.testName, func(t *testing.T) {
pb, err := EvidenceToProto(tt.evidence)
if tt.wantErr {
if tt.toProtoErr {
assert.Error(t, err, tt.testName)
return
}
assert.NoError(t, err, tt.testName)
evi, err := EvidenceFromProto(pb)
if tt.wantErr2 {
if tt.fromProtoErr {
assert.Error(t, err, tt.testName)
return
}
@ -709,29 +729,31 @@ func TestProofOfLockChangeProtoBuf(t *testing.T) {
v2 := makeVote(t, val2, chainID, math.MaxInt32, math.MaxInt64, 1, 0x01, blockID, defaultVoteTime)
testCases := []struct {
msg string
polc ProofOfLockChange
expErr bool
expErr2 bool
msg string
polc *ProofOfLockChange
toProtoErr bool
fromProtoErr bool
}{
{"failure, empty key", ProofOfLockChange{Votes: []Vote{*v, *v2}}, true, true},
{"failure, empty votes", ProofOfLockChange{PubKey: val3.PrivKey.PubKey()}, true, true},
{"success empty ProofOfLockChange", EmptyPOLC(), false, false},
{"success", ProofOfLockChange{Votes: []Vote{*v, *v2}, PubKey: val3.PrivKey.PubKey()}, false, false},
{"failure, empty key", &ProofOfLockChange{Votes: []*Vote{v, v2}, PubKey: nil}, true, false},
{"failure, empty votes", &ProofOfLockChange{PubKey: val3.PrivKey.PubKey()}, true, false},
{"success empty ProofOfLockChange", NewEmptyPOLC(), false, false},
{"success", &ProofOfLockChange{Votes: []*Vote{v, v2}, PubKey: val3.PrivKey.PubKey()}, false, false},
}
for _, tc := range testCases {
tc := tc
pbpolc, err := tc.polc.ToProto()
if tc.expErr {
if tc.toProtoErr {
assert.Error(t, err, tc.msg)
} else {
assert.NoError(t, err, tc.msg)
}
c, err := ProofOfLockChangeFromProto(pbpolc)
if !tc.expErr2 {
if !tc.fromProtoErr {
assert.NoError(t, err, tc.msg)
assert.Equal(t, &tc.polc, c, tc.msg)
if !tc.toProtoErr {
assert.Equal(t, tc.polc, c, tc.msg)
}
} else {
assert.Error(t, err, tc.msg)
}


Loading…
Cancel
Save